Recreated idbfs.js

This commit is contained in:
Alexey Olendor
2025-12-24 03:49:47 +03:00
committed by GitHub
parent 04be04a4eb
commit 91d33ccfdd
+171 -245
View File
@@ -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;