251 lines
7.8 KiB
Plaintext
251 lines
7.8 KiB
Plaintext
Module FS
|
|
EnableExplicit
|
|
|
|
;- Globals
|
|
; Pending operation state
|
|
; Safe because JS is single-threaded — one Read_ and one Write in flight at most.
|
|
Global _R_ID.s, _R_Path.s, _R_Content.s, *_R_Cb
|
|
Global _W_ID.s, _W_Path.s, _W_Content.s, *_W_Cb
|
|
|
|
;- Private declarations
|
|
Declare _FindLastSlash(Path.s)
|
|
Declare _ReadExistsCallback(Success, DataString.s)
|
|
Declare _ReadCachedCallback(Success, DataString.s)
|
|
Declare _ReadServerCallback(Success, DataString.s)
|
|
Declare _ReadCacheStoreCallback(Success, DataString.s)
|
|
Declare _WriteCacheCallback(Success, DataString.s)
|
|
Declare _WriteServerCallback(Success, DataString.s)
|
|
|
|
;- Public procedures
|
|
|
|
; List directory contents.
|
|
; DataString = JSON array of {id, name, is_dir, mime_type, size, modified_at}
|
|
Procedure List(Path.s, *Callback)
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/list?path=" + URLEncoder(Path, #PB_UTF8), "", *Callback)
|
|
EndProcedure
|
|
|
|
; Stat a single node.
|
|
; DataString = JSON object with id, name, is_dir, size, etc. or error.
|
|
Procedure Stat(Path.s, *Callback)
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", *Callback)
|
|
EndProcedure
|
|
|
|
; Read file content. Cache-first: if the file is in IndexedDB, returns it
|
|
; immediately without a server round-trip. Otherwise fetches from server
|
|
; and populates the cache for next time.
|
|
; FileID = string form of the numeric node id (from Stat or List).
|
|
; Path = virtual path, stored in cache meta for Sync to use later.
|
|
Procedure Read_(FileID.s, Path.s, *Callback)
|
|
_R_ID = FileID
|
|
_R_Path = Path
|
|
*_R_Cb = *Callback
|
|
FileCache::Exists(FileID, @_ReadExistsCallback())
|
|
EndProcedure
|
|
|
|
; Write content. Hits the cache immediately (dirty), fires the callback,
|
|
; then syncs to the server best-effort in the background.
|
|
; If the server write fails the file stays dirty — Sync() will retry it.
|
|
Procedure Write(FileID.s, Path.s, Content.s, *Callback)
|
|
_W_ID = FileID
|
|
_W_Path = Path
|
|
_W_Content = Content
|
|
*_W_Cb = *Callback
|
|
FileCache::Write(FileID, Content, @_WriteCacheCallback())
|
|
EndProcedure
|
|
|
|
; Create a directory.
|
|
; DataString = JSON {success:true, id:N} or error.
|
|
Procedure Mkdir(Path.s, *Callback)
|
|
HTTPRequest(#PB_HTTP_Post, "/api/fs/mkdir", "path=" + URLEncoder(Path, #PB_UTF8), *Callback)
|
|
EndProcedure
|
|
|
|
; Delete a node by ID. Evicts from cache too.
|
|
; DataString = JSON {success:true} or error.
|
|
Procedure Delete_(FileID.s, *Callback)
|
|
FileCache::Evict(FileID, #Null) ; best-effort, don't wait
|
|
HTTPRequest(#PB_HTTP_Post, "/api/fs/delete", "id=" + FileID, *Callback)
|
|
EndProcedure
|
|
|
|
; Move/rename a node by ID.
|
|
; DataString = JSON {success:true} or error.
|
|
Procedure Move(FileID.s, NewParentID.s, NewName.s, *Callback)
|
|
HTTPRequest(#PB_HTTP_Post, "/api/fs/move", "id=" + FileID + "&to_parent_id=" + NewParentID + "&name=" + URLEncoder(NewName, #PB_UTF8), *Callback)
|
|
EndProcedure
|
|
|
|
; Push all dirty cached files to the server sequentially.
|
|
Procedure Sync(*Callback)
|
|
!var _cb = p_callback;
|
|
!var _db = window._kumos_idb;
|
|
!if (!_db) { if (_cb) _cb(0, 'IDB not open'); return; }
|
|
!
|
|
!// Collect dirty entries from file_meta
|
|
!var tx = _db.transaction(['file_meta'], 'readonly');
|
|
!var store = tx.objectStore('file_meta');
|
|
!var req = store.openCursor();
|
|
!var dirty = [];
|
|
!
|
|
!req.onsuccess = function(ev) {
|
|
! var cursor = ev.target.result;
|
|
! if (cursor) {
|
|
! try {
|
|
! var meta = JSON.parse(cursor.value);
|
|
! if (meta.dirty) dirty.push({ id: String(cursor.key), path: meta.path });
|
|
! } catch(e) {}
|
|
! cursor.continue();
|
|
! }
|
|
!};
|
|
!
|
|
!tx.oncomplete = function() {
|
|
! if (dirty.length === 0) {
|
|
! if (_cb) _cb(1, JSON.stringify({ synced: 0, failed: 0 }));
|
|
! return;
|
|
! }
|
|
!
|
|
! var synced = 0, failed = 0, total = dirty.length;
|
|
!
|
|
! function markClean(id, onDone) {
|
|
! var wtx = _db.transaction(['file_meta'], 'readwrite');
|
|
! var ws = wtx.objectStore('file_meta');
|
|
! var gr = ws.get(id);
|
|
! gr.onsuccess = function(ev) {
|
|
! var m = {};
|
|
! try { m = JSON.parse(ev.target.result || '{}'); } catch(e) {}
|
|
! m.dirty = false;
|
|
! ws.put(JSON.stringify(m), id);
|
|
! };
|
|
! wtx.oncomplete = onDone;
|
|
! wtx.onerror = onDone;
|
|
! }
|
|
!
|
|
! function syncNext(i) {
|
|
! if (i >= total) {
|
|
! if (_cb) _cb(failed === 0 ? 1 : 0,
|
|
! JSON.stringify({ synced: synced, failed: failed }));
|
|
! return;
|
|
! }
|
|
! var entry = dirty[i];
|
|
!
|
|
! // Read content from IDB
|
|
! var rtx = _db.transaction(['file_content'], 'readonly');
|
|
! var rreq = rtx.objectStore('file_content').get(entry.id);
|
|
!
|
|
! rreq.onsuccess = function(ev) {
|
|
! var content = ev.target.result !== undefined ? String(ev.target.result) : '';
|
|
!
|
|
! // POST to server
|
|
! fetch('/api/fs/write', {
|
|
! method: 'POST',
|
|
! headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
! body: 'path=' + encodeURIComponent(entry.path) +
|
|
! '&content=' + encodeURIComponent(content)
|
|
! })
|
|
! .then(function(r) { return r.json(); })
|
|
! .then(function(json) {
|
|
! if (json.success) {
|
|
! markClean(entry.id, function() { synced++; syncNext(i + 1); });
|
|
! } else {
|
|
! failed++; syncNext(i + 1);
|
|
! }
|
|
! })
|
|
! .catch(function() { failed++; syncNext(i + 1); });
|
|
! };
|
|
!
|
|
! rreq.onerror = function() { failed++; syncNext(i + 1); };
|
|
! }
|
|
!
|
|
! syncNext(0);
|
|
!};
|
|
!
|
|
!tx.onerror = function(e) {
|
|
! if (_cb) _cb(0, e.target.error ? e.target.error.message : 'sync scan failed');
|
|
!};
|
|
EndProcedure
|
|
|
|
Procedure.s GetFilePart(Path.s)
|
|
Protected Last = _FindLastSlash(Path)
|
|
If Last = 0 : ProcedureReturn Path : EndIf
|
|
ProcedureReturn Mid(Path, Last + 1)
|
|
EndProcedure
|
|
|
|
Procedure.s GetPathPart(Path.s)
|
|
Protected Last = _FindLastSlash(Path)
|
|
If Last = 0 : ProcedureReturn "/" : EndIf ; no slash — return root
|
|
ProcedureReturn Left(Path, Last) ; includes the trailing slash
|
|
EndProcedure
|
|
|
|
;- Private procedures
|
|
|
|
Procedure _FindLastSlash(Path.s)
|
|
Protected Pos, Last
|
|
Pos = FindString(Path, "/")
|
|
While Pos > 0
|
|
Last = Pos
|
|
Pos = FindString(Path, "/", Pos + 1)
|
|
Wend
|
|
ProcedureReturn Last
|
|
EndProcedure
|
|
|
|
Procedure _ReadExistsCallback(Success, DataString.s)
|
|
Debug "Exists → Success=" + Success + " Data=" + DataString
|
|
If Success And DataString = "1"
|
|
; Cache hit — return directly
|
|
FileCache::Read_(_R_ID, @_ReadCachedCallback())
|
|
Else
|
|
; Cache miss — fetch from server
|
|
HTTPRequest(#PB_HTTP_Get, "/api/fs/read?id=" + _R_ID, "", @_ReadServerCallback())
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure _ReadCachedCallback(Success, DataString.s)
|
|
If *_R_Cb
|
|
!fs$g__r_cb(v_success, v_datastring);
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure _ReadServerCallback(Success, DataString.s)
|
|
If Success
|
|
_R_Content = DataString
|
|
FileCache::Cache(_R_ID, _R_Path, DataString, @_ReadCacheStoreCallback())
|
|
Else
|
|
!fs$g__r_cb(0, v_datastring);
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure _ReadCacheStoreCallback(Success, DataString.s)
|
|
Protected Content.s = _R_Content
|
|
If *_R_Cb
|
|
!fs$g__r_cb(1, v_content);
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure _WriteCacheCallback(Success, DataString.s)
|
|
If *_W_Cb
|
|
!fs$g__w_cb(v_success, v_datastring);
|
|
EndIf
|
|
If Success
|
|
HTTPRequest(#PB_HTTP_Post, "/api/fs/write", "path=" + URLEncoder(_W_Path, #PB_UTF8) + "&content=" + URLEncoder(_W_Content, #PB_UTF8), @_WriteServerCallback())
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure _WriteServerCallback(Success, DataString.s)
|
|
If Success
|
|
If ParseJSON(0, DataString)
|
|
If GetJSONBoolean(GetJSONMember(JSONValue(0), "success"))
|
|
FileCache::MarkClean(_W_ID, #Null)
|
|
EndIf
|
|
FreeJSON(0)
|
|
EndIf
|
|
EndIf
|
|
EndProcedure
|
|
|
|
EndModule
|
|
|
|
; IDE Options = SpiderBasic 3.20 (Windows - x86)
|
|
; CursorPosition = 21
|
|
; Folding = BAA-
|
|
; iOSAppOrientation = 0
|
|
; AndroidAppCode = 0
|
|
; AndroidAppOrientation = 0
|
|
; EnableXP
|
|
; DPIAware
|
|
; CompileSourceDirectory |