scratch0-5 / vm.files.browser.js
soiz1's picture
Upload folder using huggingface_hub
8f3f8db verified
"use strict";
/*
* 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();
},
});