Spaces:
Running
Running
| ; | |
| /* | |
| * Copyright (c) 2013-2025 Vanessa Freudenberg | |
| * | |
| * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| * of this software and associated documentation files (the "Software"), to deal | |
| * in the Software without restriction, including without limitation the rights | |
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| * copies of the Software, and to permit persons to whom the Software is | |
| * furnished to do so, subject to the following conditions: | |
| * | |
| * The above copyright notice and this permission notice shall be included in | |
| * all copies or substantial portions of the Software. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| * THE SOFTWARE. | |
| */ | |
| Object.extend(Squeak, | |
| "files", { | |
| fsck: function(whenDone, dir, files, stale, stats) { | |
| dir = dir || ""; | |
| stale = stale || {dirs: [], files: []}; | |
| stats = stats || {dirs: 0, files: 0, bytes: 0, deleted: 0}; | |
| if (!files) { | |
| // find existing files | |
| files = {}; | |
| // ... in localStorage | |
| Object.keys(Squeak.Settings).forEach(function(key) { | |
| var match = key.match(/squeak-file(\.lz)?:(.*)$/); | |
| if (match) {files[match[2]] = true}; | |
| }); | |
| // ... or in memory | |
| if (window.SqueakDBFake) Object.keys(SqueakDBFake.bigFiles).forEach(function(path) { | |
| files[path] = true; | |
| }); | |
| // ... or in IndexedDB (the normal case) | |
| if (typeof indexedDB !== "undefined") { | |
| return this.dbTransaction("readonly", "fsck cursor", function(fileStore) { | |
| var cursorReq = fileStore.openCursor(); | |
| cursorReq.onsuccess = function(e) { | |
| var cursor = e.target.result; | |
| if (cursor) { | |
| files[cursor.key] = cursor.value.byteLength; | |
| cursor.continue(); | |
| } else { // got all files | |
| Squeak.fsck(whenDone, dir, files, stale, stats); | |
| } | |
| } | |
| cursorReq.onerror = function(e) { | |
| console.error("fsck failed"); | |
| } | |
| }); | |
| } // otherwise fall through | |
| } | |
| // check directories | |
| var entries = Squeak.dirList(dir); | |
| for (var name in entries) { | |
| var path = dir + "/" + name, | |
| isDir = entries[name][3]; | |
| if (isDir) { | |
| stats.dirs++; | |
| var exists = "squeak:" + path in Squeak.Settings; | |
| if (exists) { | |
| Squeak.fsck(null, path, files, stale, stats); | |
| } else { | |
| stale.dirs.push(path); | |
| } | |
| } else { | |
| stats.files++; | |
| if (path in files) { | |
| files[path] = null; // mark as visited | |
| stats.bytes += entries[name][4]; | |
| } else { | |
| stale.files.push(path); | |
| } | |
| } | |
| } | |
| if (dir === "") { | |
| // we're back at the root, almost done | |
| console.log("squeak fsck: " + stats.dirs + " directories, " + stats.files + " files, " + (stats.bytes/1000000).toFixed(1) + " MBytes"); | |
| // check orphaned files | |
| var orphaned = [], | |
| total = 0; | |
| for (var path in files) { | |
| total++; | |
| var size = files[path]; | |
| if (size !== null) orphaned.push({ path: path, size: size }); // not marked visited | |
| } | |
| // recreate directory entries for orphaned files | |
| for (var i = 0; i < orphaned.length; i++) { | |
| var path = Squeak.splitFilePath(orphaned[i].path); | |
| var size = orphaned[i].size; | |
| console.log("squeak fsck: restoring " + path.fullname + " (" + size + " bytes)"); | |
| Squeak.dirCreate(path.dirname, true, "force"); | |
| var directory = Squeak.dirList(path.dirname); | |
| var now = Squeak.totalSeconds(); | |
| var entry = [/*name*/ path.basename, /*ctime*/ now, /*mtime*/ 0, /*dir*/ false, size]; | |
| directory[path.basename] = entry; | |
| Squeak.Settings["squeak:" + path.dirname] = JSON.stringify(directory); | |
| } | |
| for (var i = 0; i < stale.dirs.length; i++) { | |
| var dir = stale.dirs[i]; | |
| if (Squeak.Settings["squeak:" + dir]) continue; // now contains orphaned files | |
| console.log("squeak fsck: cleaning up directory " + dir); | |
| Squeak.dirDelete(dir); | |
| stats.dirs--; | |
| stats.deleted++; | |
| } | |
| for (var i = 0; i < stale.files.length; i++) { | |
| var path = stale.files[i]; | |
| if (path in files) continue; // was orphaned | |
| if (Squeak.Settings["squeak:" + path]) continue; // now is a directory | |
| console.log("squeak fsck: cleaning up file entry " + path); | |
| Squeak.fileDelete(path); | |
| stats.files--; | |
| stats.deleted++; | |
| } | |
| if (whenDone) whenDone(stats); | |
| } | |
| }, | |
| dbTransaction: function(mode, description, transactionFunc, completionFunc) { | |
| // File contents is stored in the IndexedDB named "squeak" in object store "files" | |
| // and directory entries in localStorage with prefix "squeak:" | |
| function fakeTransaction() { | |
| transactionFunc(Squeak.dbFake()); | |
| if (completionFunc) completionFunc(); | |
| } | |
| if (typeof indexedDB == "undefined") { | |
| return fakeTransaction(); | |
| } | |
| function startTransaction() { | |
| var trans = SqueakDB.transaction("files", mode), | |
| fileStore = trans.objectStore("files"); | |
| trans.oncomplete = function(e) { if (completionFunc) completionFunc(); } | |
| trans.onerror = function(e) { console.error("Transaction error during " + description, e); } | |
| trans.onabort = function(e) { | |
| console.error("Transaction error: aborting " + description, e); | |
| // fall back to local/memory storage | |
| transactionFunc(Squeak.dbFake()); | |
| if (completionFunc) completionFunc(); | |
| } | |
| transactionFunc(fileStore); | |
| }; | |
| // if database connection already opened, just do transaction | |
| if (window.SqueakDB) return startTransaction(); | |
| // otherwise, open SqueakDB first | |
| var openReq; | |
| try { | |
| // fails in restricted iframe | |
| openReq = indexedDB.open("squeak"); | |
| } catch (err) {} | |
| // UIWebView implements the interface but only returns null | |
| // https://stackoverflow.com/questions/27415998/indexeddb-open-returns-null-on-safari-ios-8-1-1-and-halts-execution-on-cordova | |
| if (!openReq) { | |
| return fakeTransaction(); | |
| } | |
| openReq.onsuccess = function(e) { | |
| if (Squeak.debugFiles) console.log("Opened files database."); | |
| window.SqueakDB = this.result; | |
| SqueakDB.onversionchange = function(e) { | |
| delete window.SqueakDB; | |
| this.close(); | |
| }; | |
| SqueakDB.onerror = function(e) { | |
| console.error("Error accessing database", e); | |
| }; | |
| startTransaction(); | |
| }; | |
| openReq.onupgradeneeded = function (e) { | |
| // run only first time, or when version changed | |
| if (Squeak.debugFiles) console.log("Creating files database"); | |
| var db = e.target.result; | |
| db.createObjectStore("files"); | |
| }; | |
| openReq.onerror = function(e) { | |
| console.error("Error opening files database", e); | |
| console.warn("Falling back to local storage"); | |
| fakeTransaction(); | |
| }; | |
| openReq.onblocked = function(e) { | |
| // If some other tab is loaded with the database, then it needs to be closed | |
| // before we can proceed upgrading the database. | |
| console.log("Database upgrade needed, but was blocked."); | |
| console.warn("Falling back to local storage"); | |
| fakeTransaction(); | |
| }; | |
| }, | |
| dbFake: function() { | |
| // indexedDB is not supported by this browser, fake it using localStorage | |
| // since localStorage space is severly limited, use LZString if loaded | |
| // see https://github.com/pieroxy/lz-string | |
| if (typeof SqueakDBFake == "undefined") { | |
| if (typeof indexedDB == "undefined") | |
| console.warn("IndexedDB not supported by this browser, using localStorage"); | |
| window.SqueakDBFake = { | |
| bigFiles: {}, | |
| bigFileThreshold: 100000, | |
| get: function(filename) { | |
| var buffer = SqueakDBFake.bigFiles[filename]; | |
| if (!buffer) { | |
| var string = Squeak.Settings["squeak-file:" + filename]; | |
| if (!string) { | |
| var compressed = Squeak.Settings["squeak-file.lz:" + filename]; | |
| if (compressed) { | |
| if (typeof LZString == "object") { | |
| string = LZString.decompressFromUTF16(compressed); | |
| } else { | |
| console.error("LZString not loaded: cannot decompress " + filename); | |
| } | |
| } | |
| } | |
| if (string) { | |
| var bytes = new Uint8Array(string.length); | |
| for (var i = 0; i < bytes.length; i++) | |
| bytes[i] = string.charCodeAt(i) & 0xFF; | |
| buffer = bytes.buffer; | |
| } | |
| } | |
| var req = {result: buffer}; | |
| setTimeout(function(){ | |
| if (req.onsuccess) req.onsuccess({target: req}); | |
| }, 0); | |
| return req; | |
| }, | |
| put: function(buffer, filename) { | |
| if (buffer.byteLength > SqueakDBFake.bigFileThreshold) { | |
| if (!SqueakDBFake.bigFiles[filename]) | |
| console.log("File " + filename + " (" + buffer.byteLength + " bytes) too large, storing in memory only"); | |
| SqueakDBFake.bigFiles[filename] = buffer; | |
| } else { | |
| var string = Squeak.bytesAsString(new Uint8Array(buffer)); | |
| if (typeof LZString == "object") { | |
| var compressed = LZString.compressToUTF16(string); | |
| Squeak.Settings["squeak-file.lz:" + filename] = compressed; | |
| delete Squeak.Settings["squeak-file:" + filename]; | |
| } else { | |
| Squeak.Settings["squeak-file:" + filename] = string; | |
| } | |
| } | |
| var req = {}; | |
| setTimeout(function(){if (req.onsuccess) req.onsuccess()}, 0); | |
| return req; | |
| }, | |
| delete: function(filename) { | |
| delete Squeak.Settings["squeak-file:" + filename]; | |
| delete Squeak.Settings["squeak-file.lz:" + filename]; | |
| delete SqueakDBFake.bigFiles[filename]; | |
| var req = {}; | |
| setTimeout(function(){if (req.onsuccess) req.onsuccess()}, 0); | |
| return req; | |
| }, | |
| openCursor: function() { | |
| var req = {}; | |
| setTimeout(function(){if (req.onsuccess) req.onsuccess({target: req})}, 0); | |
| return req; | |
| }, | |
| } | |
| } | |
| return SqueakDBFake; | |
| }, | |
| fileGet: function(filepath, thenDo, errorDo) { | |
| if (!errorDo) errorDo = function(err) { console.log(err) }; | |
| var path = this.splitFilePath(filepath); | |
| if (!path.basename) return errorDo("Invalid path: " + filepath); | |
| if (Squeak.debugFiles) { | |
| console.log("Reading " + path.fullname); | |
| var realThenDo = thenDo; | |
| thenDo = function(data) { | |
| console.log("Read " + data.byteLength + " bytes from " + path.fullname); | |
| realThenDo(data); | |
| } | |
| } | |
| // if we have been writing to memory, return that version | |
| if (window.SqueakDBFake && SqueakDBFake.bigFiles[path.fullname]) | |
| return thenDo(SqueakDBFake.bigFiles[path.fullname]); | |
| this.dbTransaction("readonly", "get " + filepath, function(fileStore) { | |
| var getReq = fileStore.get(path.fullname); | |
| getReq.onerror = function(e) { errorDo(e) }; | |
| getReq.onsuccess = function(e) { | |
| if (this.result !== undefined) return thenDo(this.result); | |
| // might be a template | |
| Squeak.fetchTemplateFile(path.fullname, | |
| function gotTemplate(template) {thenDo(template)}, | |
| function noTemplate() { | |
| // if no indexedDB then we have checked fake db already | |
| if (typeof indexedDB == "undefined") return errorDo("file not found: " + path.fullname); | |
| // fall back on fake db, may be file is there | |
| var fakeReq = Squeak.dbFake().get(path.fullname); | |
| fakeReq.onerror = function(e) { errorDo("file not found: " + path.fullname) }; | |
| fakeReq.onsuccess = function(e) { thenDo(this.result); } | |
| }); | |
| }; | |
| }); | |
| }, | |
| filePut: function(filepath, contents, optSuccess) { | |
| // store file, return dir entry if successful | |
| var path = this.splitFilePath(filepath); if (!path.basename) return null; | |
| var directory = this.dirList(path.dirname); if (!directory) return null; | |
| // get or create entry | |
| var entry = directory[path.basename], | |
| now = this.totalSeconds(); | |
| if (!entry) { // new file | |
| entry = [/*name*/ path.basename, /*ctime*/ now, /*mtime*/ 0, /*dir*/ false, /*size*/ 0]; | |
| directory[path.basename] = entry; | |
| } else if (entry[3]) // is a directory | |
| return null; | |
| if (Squeak.debugFiles) { | |
| console.log("Writing " + path.fullname + " (" + contents.byteLength + " bytes)"); | |
| if (contents.byteLength > 0 && filepath.endsWith(".log")) { | |
| console.log((new TextDecoder).decode(contents).replace(/\r/g, '\n')); | |
| } | |
| } | |
| // update directory entry | |
| entry[2] = now; // modification time | |
| entry[4] = contents.byteLength || contents.length || 0; | |
| Squeak.Settings["squeak:" + path.dirname] = JSON.stringify(directory); | |
| // put file contents (async) | |
| this.dbTransaction("readwrite", "put " + filepath, | |
| function(fileStore) { | |
| fileStore.put(contents, path.fullname); | |
| }, | |
| function transactionComplete() { | |
| if (optSuccess) optSuccess(); | |
| }); | |
| return entry; | |
| }, | |
| fileDelete: function(filepath, entryOnly) { | |
| var path = this.splitFilePath(filepath); if (!path.basename) return false; | |
| var directory = this.dirList(path.dirname); if (!directory) return false; | |
| var entry = directory[path.basename]; if (!entry || entry[3]) return false; // not found or is a directory | |
| // delete entry from directory | |
| delete directory[path.basename]; | |
| Squeak.Settings["squeak:" + path.dirname] = JSON.stringify(directory); | |
| if (Squeak.debugFiles) console.log("Deleting " + path.fullname); | |
| if (entryOnly) return true; | |
| // delete file contents (async) | |
| this.dbTransaction("readwrite", "delete " + filepath, function(fileStore) { | |
| fileStore.delete(path.fullname); | |
| }); | |
| return true; | |
| }, | |
| fileRename: function(from, to) { | |
| var oldpath = this.splitFilePath(from); if (!oldpath.basename) return false; | |
| var newpath = this.splitFilePath(to); if (!newpath.basename) return false; | |
| var olddir = this.dirList(oldpath.dirname); if (!olddir) return false; | |
| var entry = olddir[oldpath.basename]; if (!entry || entry[3]) return false; // not found or is a directory | |
| var samedir = oldpath.dirname == newpath.dirname; | |
| var newdir = samedir ? olddir : this.dirList(newpath.dirname); if (!newdir) return false; | |
| if (newdir[newpath.basename]) return false; // exists already | |
| if (Squeak.debugFiles) console.log("Renaming " + oldpath.fullname + " to " + newpath.fullname); | |
| delete olddir[oldpath.basename]; // delete old entry | |
| entry[0] = newpath.basename; // rename entry | |
| newdir[newpath.basename] = entry; // add new entry | |
| Squeak.Settings["squeak:" + newpath.dirname] = JSON.stringify(newdir); | |
| if (!samedir) Squeak.Settings["squeak:" + oldpath.dirname] = JSON.stringify(olddir); | |
| // move file contents (async) | |
| this.fileGet(oldpath.fullname, | |
| function success(contents) { | |
| this.dbTransaction("readwrite", "rename " + oldpath.fullname + " to " + newpath.fullname, function(fileStore) { | |
| fileStore.delete(oldpath.fullname); | |
| fileStore.put(contents, newpath.fullname); | |
| }); | |
| }.bind(this), | |
| function error(msg) { | |
| console.log("File rename failed: " + msg); | |
| }.bind(this)); | |
| return true; | |
| }, | |
| fileExists: function(filepath) { | |
| var path = this.splitFilePath(filepath); if (!path.basename) return false; | |
| var directory = this.dirList(path.dirname); if (!directory) return false; | |
| var entry = directory[path.basename]; if (!entry || entry[3]) return false; // not found or is a directory | |
| return true; | |
| }, | |
| dirCreate: function(dirpath, withParents, force) { | |
| var path = this.splitFilePath(dirpath); if (!path.basename) return false; | |
| if (withParents && !Squeak.Settings["squeak:" + path.dirname]) Squeak.dirCreate(path.dirname, true); | |
| var parent = this.dirList(path.dirname); if (!parent) return false; | |
| var existing = parent[path.basename]; | |
| if (existing) { | |
| if (!existing[3]) { | |
| // already exists and is not a directory | |
| if (!force) return false; | |
| existing[3] = true; // force it to be a directory | |
| Squeak.Settings["squeak:" + path.dirname] = JSON.stringify(parent); | |
| } | |
| if (Squeak.Settings["squeak:" + path.fullname]) return true; // already exists | |
| // directory exists but is not in localStorage, so create it | |
| // (this is not supposed to happen but deal with it anyways) | |
| } | |
| if (Squeak.debugFiles) console.log("Creating directory " + path.fullname); | |
| var now = this.totalSeconds(), | |
| entry = [/*name*/ path.basename, /*ctime*/ now, /*mtime*/ now, /*dir*/ true, /*size*/ 0]; | |
| parent[path.basename] = entry; | |
| Squeak.Settings["squeak:" + path.fullname] = JSON.stringify({}); | |
| Squeak.Settings["squeak:" + path.dirname] = JSON.stringify(parent); | |
| return true; | |
| }, | |
| dirDelete: function(dirpath) { | |
| var path = this.splitFilePath(dirpath); if (!path.basename) return false; | |
| var directory = this.dirList(path.dirname); if (!directory) return false; | |
| if (!directory[path.basename]) return false; | |
| var children = this.dirList(path.fullname); | |
| if (children) for (var child in children) return false; // not empty | |
| if (Squeak.debugFiles) console.log("Deleting directory " + path.fullname); | |
| // delete from parent | |
| delete directory[path.basename]; | |
| Squeak.Settings["squeak:" + path.dirname] = JSON.stringify(directory); | |
| // delete itself | |
| delete Squeak.Settings["squeak:" + path.fullname]; | |
| return true; | |
| }, | |
| dirList: function(dirpath, includeTemplates) { | |
| // return directory entries or null | |
| var path = this.splitFilePath(dirpath), | |
| localEntries = Squeak.Settings["squeak:" + path.fullname], | |
| template = includeTemplates && Squeak.Settings["squeak-template:" + path.fullname]; | |
| function addEntries(dir, entries) { | |
| for (var key in entries) { | |
| if (entries.hasOwnProperty(key)) { | |
| var entry = entries[key]; | |
| dir[entry[0]] = entry; | |
| } | |
| } | |
| } | |
| if (localEntries || template) { | |
| // local entries override templates | |
| var dir = {}; | |
| if (template) addEntries(dir, JSON.parse(template).entries); | |
| if (localEntries) addEntries(dir, JSON.parse(localEntries)); | |
| return dir; | |
| } | |
| if (path.fullname == "/") return {}; | |
| return null; | |
| }, | |
| splitFilePath: function(filepath) { | |
| if (filepath[0] !== '/') filepath = '/' + filepath; | |
| filepath = filepath.replace(/\/\//g, '/'); // replace double-slashes | |
| var matches = filepath.match(/(.*)\/(.*)/), | |
| dirname = matches[1] ? matches[1] : '/', | |
| basename = matches[2] ? matches[2] : null; | |
| return {fullname: filepath, dirname: dirname, basename: basename}; | |
| }, | |
| splitUrl: function(url, base) { | |
| var matches = url.match(/(.*\/)?(.*)/), | |
| uptoslash = matches[1] || '', | |
| filename = matches[2] || ''; | |
| if (!uptoslash.match(/^[a-z]+:\/\//)) { | |
| if (base && !base.match(/\/$/)) base += '/'; | |
| uptoslash = (base || '') + uptoslash; | |
| url = uptoslash + filename; | |
| } | |
| return {full: url, uptoslash: uptoslash, filename: filename}; | |
| }, | |
| flushFile: function(file) { | |
| if (file.modified) { | |
| var buffer = file.contents.buffer; | |
| if (buffer.byteLength !== file.size) { | |
| buffer = new ArrayBuffer(file.size); | |
| (new Uint8Array(buffer)).set(file.contents.subarray(0, file.size)); | |
| } | |
| Squeak.filePut(file.name, buffer); | |
| file.modified = false; | |
| } | |
| }, | |
| flushAllFiles: function() { | |
| if (typeof SqueakFiles == 'undefined') return; | |
| for (var name in SqueakFiles) | |
| this.flushFile(SqueakFiles[name]); | |
| }, | |
| closeAllFiles: function() { | |
| // close the files held open in memory | |
| Squeak.flushAllFiles(); | |
| delete window.SqueakFiles; | |
| }, | |
| fetchTemplateDir: function(path, url) { | |
| // Called on app startup. Fetch url/sqindex.json and | |
| // cache all subdirectory entries in Squeak.Settings. | |
| // File contents is only fetched on demand | |
| path = Squeak.splitFilePath(path).fullname; | |
| function ensureTemplateParent(template) { | |
| var path = Squeak.splitFilePath(template); | |
| if (path.dirname !== "/") ensureTemplateParent(path.dirname); | |
| var template = JSON.parse(Squeak.Settings["squeak-template:" + path.dirname] || '{"entries": {}}'); | |
| if (!template.entries[path.basename]) { | |
| var now = Squeak.totalSeconds(); | |
| template.entries[path.basename] = [path.basename, now, now, true, 0]; | |
| Squeak.Settings["squeak-template:" + path.dirname] = JSON.stringify(template); | |
| } | |
| } | |
| function checkSubTemplates(path, url) { | |
| var template = JSON.parse(Squeak.Settings["squeak-template:" + path]); | |
| for (var key in template.entries) { | |
| var entry = template.entries[key]; | |
| if (entry[3]) Squeak.fetchTemplateDir(path + "/" + entry[0], url + "/" + entry[0]); | |
| }; | |
| } | |
| if (Squeak.Settings["squeak-template:" + path]) { | |
| checkSubTemplates(path, url); | |
| } else { | |
| var index = url + "/sqindex.json"; | |
| var rq = new XMLHttpRequest(); | |
| rq.open('GET', index, true); | |
| rq.onload = function(e) { | |
| if (rq.status == 200) { | |
| console.log("adding template dir " + path); | |
| ensureTemplateParent(path); | |
| var entries = JSON.parse(rq.response), | |
| template = {url: url, entries: {}}; | |
| for (var key in entries) { | |
| var entry = entries[key]; | |
| template.entries[entry[0]] = entry; | |
| } | |
| Squeak.Settings["squeak-template:" + path] = JSON.stringify(template); | |
| checkSubTemplates(path, url); | |
| } | |
| else rq.onerror(rq.statusText); | |
| }; | |
| rq.onerror = function(e) { | |
| console.log("cannot load template index " + index); | |
| } | |
| rq.send(); | |
| } | |
| }, | |
| fetchTemplateFile: function(path, ifFound, ifNotFound) { | |
| path = Squeak.splitFilePath(path); | |
| var template = Squeak.Settings["squeak-template:" + path.dirname]; | |
| if (!template) return ifNotFound(); | |
| var url = JSON.parse(template).url; | |
| if (!url) return ifNotFound(); | |
| url += "/" + path.basename; | |
| var rq = new XMLHttpRequest(); | |
| rq.open("get", url, true); | |
| rq.responseType = "arraybuffer"; | |
| rq.timeout = 30000; | |
| rq.onreadystatechange = function() { | |
| if (this.readyState != this.DONE) return; | |
| if (this.status == 200) { | |
| var buffer = this.response; | |
| console.log("Got " + buffer.byteLength + " bytes from " + url); | |
| Squeak.dirCreate(path.dirname, true); | |
| Squeak.filePut(path.fullname, buffer); | |
| ifFound(buffer); | |
| } else { | |
| console.error("Download failed (" + this.status + ") " + url); | |
| ifNotFound(); | |
| } | |
| } | |
| console.log("Fetching " + url); | |
| rq.send(); | |
| }, | |
| }); | |