mirror of
https://github.com/Lolendor/reVCDOS.git
synced 2026-10-10 11:53:11 +08:00
Recreated idbfs.js
This commit is contained in:
Vendored
+171
-245
@@ -1,310 +1,236 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* IDBFS Wrapper for synchronization and persistence.
|
||||
* This module provides a way to synchronize an IndexedDB-based file system (IDBFS)
|
||||
* with remote storage or other listeners.
|
||||
*/
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* Writes a 32-bit unsigned integer to a Uint8Array in little-endian format.
|
||||
* @param {Uint8Array} buffer
|
||||
* @param {number} value
|
||||
* @param {number} offset
|
||||
* @returns {number} New offset
|
||||
*/
|
||||
function writeUint32(buffer, value, offset) {
|
||||
buffer[offset] = value & 0xFF;
|
||||
buffer[offset + 1] = (value & 0xFF00) >> 8;
|
||||
buffer[offset + 2] = (value & 0xFF0000) >> 16;
|
||||
buffer[offset + 3] = (value & 0xFF000000) >> 24;
|
||||
return offset + 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a 32-bit unsigned integer from a Uint8Array in little-endian format.
|
||||
* @param {Uint8Array} buffer
|
||||
* @param {number} offset
|
||||
* @returns {number}
|
||||
*/
|
||||
function readUint32(buffer, offset) {
|
||||
return (buffer[offset] & 0xFF) |
|
||||
((buffer[offset + 1] << 8) & 0xFF00) |
|
||||
((buffer[offset + 2] << 16) & 0xFF0000) |
|
||||
((buffer[offset + 3] << 24) & 0xFF000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a 64-bit timestamp (milliseconds) to a Uint8Array as two 32-bit integers.
|
||||
* @param {Uint8Array} buffer
|
||||
* @param {number} value
|
||||
* @param {number} offset
|
||||
* @returns {number} New offset
|
||||
*/
|
||||
function writeUint64(buffer, value, offset) {
|
||||
const high = (value / 0x100000000) >>> 0;
|
||||
const low = value >>> 0;
|
||||
writeUint32(buffer, low, offset);
|
||||
writeUint32(buffer, high, offset + 4);
|
||||
return offset + 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a 64-bit timestamp from a Uint8Array.
|
||||
* @param {Uint8Array} buffer
|
||||
* @param {number} offset
|
||||
* @returns {number}
|
||||
*/
|
||||
function readUint64(buffer, offset) {
|
||||
const low = readUint32(buffer, offset);
|
||||
const high = readUint32(buffer, offset + 4);
|
||||
return (high * 0x100000000) + low;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a binary buffer into an array of file entries.
|
||||
* Format: [pathLen(4)][path(pathLen)][timestamp(8)][mode(4)][hasContents(1)][contentsLen(4)?][contents?]
|
||||
* @param {Uint8Array} buffer
|
||||
* @returns {Array}
|
||||
*/
|
||||
function deserializeEntries(buffer) {
|
||||
const entries = [];
|
||||
let offset = 0;
|
||||
while (offset < buffer.length) {
|
||||
const pathLength = readUint32(buffer, offset);
|
||||
offset += 4;
|
||||
|
||||
const path = textDecoder.decode(buffer.subarray(offset, offset + pathLength));
|
||||
offset += pathLength;
|
||||
|
||||
const timestamp = readUint64(buffer, offset);
|
||||
offset += 8;
|
||||
|
||||
const mode = readUint32(buffer, offset);
|
||||
offset += 4;
|
||||
|
||||
const hasContents = buffer[offset] === 1;
|
||||
offset += 1;
|
||||
|
||||
let contents;
|
||||
if (hasContents) {
|
||||
const contentsLength = readUint32(buffer, offset);
|
||||
offset += 4;
|
||||
contents = buffer.subarray(offset, offset + contentsLength);
|
||||
offset += contentsLength;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
path: path,
|
||||
timestamp: new Date(timestamp),
|
||||
mode: mode,
|
||||
contents: contents
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an array of file entries into a binary buffer.
|
||||
* @param {Array} entries
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
function serializeEntries(entries) {
|
||||
let totalSize = 0;
|
||||
const preparedEntries = entries.map(entry => {
|
||||
const encodedPath = textEncoder.encode(entry.path);
|
||||
let entrySize = 4 + encodedPath.length + 8 + 4 + 1;
|
||||
if (entry.contents) {
|
||||
entrySize += 4 + entry.contents.length;
|
||||
}
|
||||
totalSize += entrySize;
|
||||
return {
|
||||
encodedPath: encodedPath,
|
||||
timestamp: entry.timestamp.getTime(),
|
||||
mode: entry.mode,
|
||||
contents: entry.contents
|
||||
};
|
||||
});
|
||||
|
||||
const buffer = new Uint8Array(totalSize);
|
||||
let offset = 0;
|
||||
for (const entry of preparedEntries) {
|
||||
offset = writeUint32(buffer, entry.encodedPath.length, offset);
|
||||
buffer.set(entry.encodedPath, offset);
|
||||
offset += entry.encodedPath.length;
|
||||
|
||||
offset = writeUint64(buffer, entry.timestamp, offset);
|
||||
offset = writeUint32(buffer, entry.mode, offset);
|
||||
|
||||
buffer[offset] = entry.contents ? 1 : 0;
|
||||
offset += 1;
|
||||
|
||||
if (entry.contents) {
|
||||
offset = writeUint32(buffer, entry.contents.length, offset);
|
||||
buffer.set(entry.contents, offset);
|
||||
offset += entry.contents.length;
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an IDBFS instance to provide synchronization capabilities.
|
||||
* @param {function} logger (logger func)
|
||||
*/
|
||||
function wrapIDBFS(logger) {
|
||||
const onLoadListeners = [];
|
||||
const onSaveListeners = [];
|
||||
|
||||
/**
|
||||
* Helper to get the IndexedDB database instance for a mountpoint.
|
||||
*/
|
||||
function getDatabase(idbfs_instance, mountpoint) {
|
||||
function getDB(instance, mount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
idbfs_instance.getDB(mountpoint, (err, db) => {
|
||||
instance.getDB(mount.mountpoint, (err, db) => {
|
||||
if (err) return reject(err);
|
||||
resolve(db);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves remote entries into the local IndexedDB.
|
||||
*/
|
||||
async function saveRemoteEntries(idbfs_instance, mountpoint, entries) {
|
||||
const db = await getDatabase(idbfs_instance, mountpoint);
|
||||
async function saveToIDBFS(instance, mount, entries) {
|
||||
const db = await getDB(instance, mount);
|
||||
return new Promise((resolve, reject) => {
|
||||
(async function() {
|
||||
const transaction = db.transaction([idbfs_instance.DB_STORE_NAME], "readwrite");
|
||||
const store = transaction.objectStore(idbfs_instance.DB_STORE_NAME);
|
||||
(async function () {
|
||||
const tx = db.transaction([instance.DB_STORE_NAME], "readwrite");
|
||||
const store = tx.objectStore(instance.DB_STORE_NAME);
|
||||
|
||||
for (const entry of entries) {
|
||||
await new Promise((res, rej) => {
|
||||
idbfs_instance.storeRemoteEntry(store, entry.path, entry, (err) => {
|
||||
if (err) return rej(err);
|
||||
res();
|
||||
await new Promise((resolveStore, rejectStore) => {
|
||||
instance.storeRemoteEntry(store, entry.path, entry, (err) => {
|
||||
if (err) return rejectStore(err);
|
||||
resolveStore();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
transaction.onerror = (event) => {
|
||||
reject(event);
|
||||
event.preventDefault();
|
||||
|
||||
tx.onerror = (e) => {
|
||||
reject(e);
|
||||
e.preventDefault();
|
||||
};
|
||||
transaction.oncomplete = () => {
|
||||
tx.oncomplete = () => {
|
||||
resolve();
|
||||
};
|
||||
})().catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all entries in the local IndexedDB for a mountpoint.
|
||||
*/
|
||||
async function clearDatabase(idbfs_instance, mountpoint) {
|
||||
const db = await getDatabase(idbfs_instance, mountpoint);
|
||||
const transaction = db.transaction([idbfs_instance.DB_STORE_NAME], "readwrite");
|
||||
const store = transaction.objectStore(idbfs_instance.DB_STORE_NAME);
|
||||
async function clearIDBFS(instance, mount) {
|
||||
const db = await getDB(instance, mount);
|
||||
const tx = db.transaction([instance.DB_STORE_NAME], "readwrite");
|
||||
const store = tx.objectStore(instance.DB_STORE_NAME);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const request = store.clear();
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
const req = store.clear();
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main synchronization function.
|
||||
* Replaces the default idbfs_instance.syncfs.
|
||||
*/
|
||||
window.syncfs = (idbfs_instance, mountpoint, isRemoteToLocal, onComplete, onReady) => {
|
||||
(async function() {
|
||||
// Phase 1: Load from remote to local
|
||||
if (isRemoteToLocal) {
|
||||
// Helper functions for binary encoding/decoding
|
||||
function writeInt32(buffer, value, offset) {
|
||||
buffer[offset] = value & 255;
|
||||
buffer[offset + 1] = (value & 65280) >> 8;
|
||||
buffer[offset + 2] = (value & 16711680) >> 16;
|
||||
buffer[offset + 3] = (value & 4278190080) >> 24;
|
||||
return offset + 4;
|
||||
}
|
||||
|
||||
function readInt32(buffer, offset) {
|
||||
return (buffer[offset] & 255) |
|
||||
((buffer[offset + 1] << 8) & 65280) |
|
||||
((buffer[offset + 2] << 16) & 16711680) |
|
||||
((buffer[offset + 3] << 24) & 4278190080);
|
||||
}
|
||||
|
||||
function writeInt64(buffer, value, offset) {
|
||||
const high = (value / 4294967296) >>> 0;
|
||||
writeInt32(buffer, value >>> 0, offset);
|
||||
writeInt32(buffer, high, offset + 4);
|
||||
return offset + 8;
|
||||
}
|
||||
|
||||
function readInt64(buffer, offset) {
|
||||
const low = readInt32(buffer, offset);
|
||||
const high = readInt32(buffer, offset + 4);
|
||||
return 4294967296 * high + low;
|
||||
}
|
||||
|
||||
function decodeEntries(buffer) {
|
||||
const entries = [];
|
||||
let offset = 0;
|
||||
while (offset < buffer.length) {
|
||||
const pathLen = readInt32(buffer, offset);
|
||||
offset += 4;
|
||||
const path = textDecoder.decode(buffer.subarray(offset, offset + pathLen));
|
||||
offset += pathLen;
|
||||
|
||||
const timestamp = readInt64(buffer, offset);
|
||||
offset += 8;
|
||||
|
||||
const mode = readInt32(buffer, offset);
|
||||
offset += 4;
|
||||
|
||||
const hasContents = buffer[offset] === 1;
|
||||
offset += 1;
|
||||
|
||||
let contents;
|
||||
if (hasContents) {
|
||||
const contentLen = readInt32(buffer, offset);
|
||||
offset += 4;
|
||||
contents = buffer.subarray(offset, offset + contentLen);
|
||||
offset += contentLen;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
path: path,
|
||||
timestamp: new Date(timestamp),
|
||||
mode: mode,
|
||||
contents: contents
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function encodeEntries(entries) {
|
||||
let size = 0;
|
||||
const processedEntries = entries.map(entry => {
|
||||
const encodedPath = textEncoder.encode(entry.path);
|
||||
size += 4 + encodedPath.length + 8 + 4 + 1 + (entry.contents ? 4 + entry.contents.length : 0);
|
||||
return {
|
||||
key: encodedPath,
|
||||
time: entry.timestamp.getTime(),
|
||||
mode: entry.mode,
|
||||
contents: entry.contents
|
||||
};
|
||||
});
|
||||
|
||||
const buffer = new Uint8Array(size);
|
||||
let offset = 0;
|
||||
|
||||
for (const entry of processedEntries) {
|
||||
offset = writeInt32(buffer, entry.key.length, offset);
|
||||
buffer.set(entry.key, offset);
|
||||
offset += entry.key.length;
|
||||
|
||||
offset = writeInt64(buffer, entry.time, offset);
|
||||
offset = writeInt32(buffer, entry.mode, offset);
|
||||
|
||||
buffer[offset] = entry.contents ? 1 : 0;
|
||||
offset += 1;
|
||||
|
||||
if (entry.contents) {
|
||||
offset = writeInt32(buffer, entry.contents.length, offset);
|
||||
buffer.set(entry.contents, offset);
|
||||
offset += entry.contents.length;
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async function getLocalEntries(instance, mount) {
|
||||
const localSet = await new Promise((resolve, reject) => {
|
||||
instance.getLocalSet(mount, (err, set) => {
|
||||
if (err) return reject(err);
|
||||
resolve(set);
|
||||
});
|
||||
});
|
||||
|
||||
const entries = [];
|
||||
for (const path of Object.keys(localSet.entries)) {
|
||||
const entry = await new Promise((resolve, reject) => {
|
||||
instance.loadLocalEntry(path, (err, ent) => {
|
||||
if (err) return reject(err);
|
||||
resolve(ent);
|
||||
});
|
||||
});
|
||||
entry.path = path;
|
||||
entries.push(entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
window.syncfs = (instance, mount, populate, callback, originalSync) => {
|
||||
(async function () {
|
||||
if (populate) {
|
||||
for (const listener of onLoadListeners) {
|
||||
const data = await listener(idbfs_instance, mountpoint);
|
||||
const data = await listener(instance, mount);
|
||||
if (data) {
|
||||
await clearDatabase(idbfs_instance, mountpoint);
|
||||
await clearIDBFS(instance, mount);
|
||||
if (data.length > 0) {
|
||||
const entries = deserializeEntries(data);
|
||||
await saveRemoteEntries(idbfs_instance, mountpoint, entries);
|
||||
const entries = decodeEntries(data);
|
||||
await saveToIDBFS(instance, mount, entries);
|
||||
}
|
||||
// Stop after the first listener that provides data
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Signal readiness and handle local-to-remote sync
|
||||
onReady((callback) => {
|
||||
// Call the original completion callback
|
||||
onComplete(callback);
|
||||
originalSync((err) => {
|
||||
callback(err);
|
||||
|
||||
(async function() {
|
||||
// If we are doing local-to-remote sync
|
||||
if (!isRemoteToLocal) {
|
||||
let cachedSerializedData = null;
|
||||
|
||||
/**
|
||||
* Lazy-serializes local entries for listeners.
|
||||
*/
|
||||
const getSerializedData = async () => {
|
||||
if (cachedSerializedData === null) {
|
||||
const localEntries = await (async function() {
|
||||
const localSet = await new Promise((resolve, reject) => {
|
||||
idbfs_instance.getLocalSet(mountpoint, (err, set) => {
|
||||
if (err) return reject(err);
|
||||
resolve(set);
|
||||
});
|
||||
});
|
||||
|
||||
const entries = [];
|
||||
for (const path of Object.keys(localSet.entries)) {
|
||||
const entry = await new Promise((resolve, reject) => {
|
||||
idbfs_instance.loadLocalEntry(path, (err, e) => {
|
||||
if (err) return reject(err);
|
||||
resolve(e);
|
||||
});
|
||||
});
|
||||
entry.path = path;
|
||||
entries.push(entry);
|
||||
}
|
||||
return entries;
|
||||
})();
|
||||
|
||||
cachedSerializedData = serializeEntries(localEntries);
|
||||
(async function () {
|
||||
if (!populate) {
|
||||
let cachedData = null;
|
||||
const getData = async () => {
|
||||
if (cachedData === null) {
|
||||
const entries = await getLocalEntries(instance, mount);
|
||||
cachedData = encodeEntries(entries);
|
||||
}
|
||||
return cachedSerializedData;
|
||||
return cachedData;
|
||||
};
|
||||
|
||||
// Notify all save listeners
|
||||
for (const listener of onSaveListeners) {
|
||||
listener(getSerializedData, idbfs_instance, mountpoint);
|
||||
listener(getData, instance, mount);
|
||||
}
|
||||
}
|
||||
})().catch(err => {
|
||||
logger("ERR!!! syncfs error", err);
|
||||
});
|
||||
});
|
||||
|
||||
})().catch(err => {
|
||||
logger("ERR!!! syncfs error", err);
|
||||
onComplete(err);
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Adds a listener for load and save events.
|
||||
* @param {Object} listener { onLoad: function, onSave: function }
|
||||
*/
|
||||
addListener: (listener) => {
|
||||
if (listener.onLoad) onLoadListeners.push(listener.onLoad);
|
||||
if (listener.onSave) onSaveListeners.push(listener.onSave);
|
||||
onLoadListeners.push(listener.onLoad);
|
||||
onSaveListeners.push(listener.onSave);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Export to window
|
||||
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
||||
module.exports.wrapIDBFS = wrapIDBFS;
|
||||
}
|
||||
window.wrapIDBFS = wrapIDBFS;
|
||||
|
||||
Reference in New Issue
Block a user