3

I'm trying to create a web application that can be used via a file:// URI. This means that I can't use AJAX to load binary files (without turning off security features in the browser, which I don't want to do as a matter of principle).

The application uses a SQLite database. I want to provide the database to a sql.js constructor, which requires it in Uint8Array format.

Since I can't use AJAX to load the database file, I could instead load it with <input type="file"> and FileReader.prototype.readAsArrayBuffer and convert the ArrayBuffer to a Uint8Array. And that's working with the following code:

input.addEventListener('change', function (changeEvent) {
  var file = changeEvent.currentTarget.files[0];
  var reader = new FileReader();
  reader.addEventListener('load', function (loadEvent) {
    var buffer = loadEvent.target.result;
    var uint8Array = new Uint8Array(buffer);
    var db = new sql.Database(uint8Array);
  });
  reader.readAsArrayBuffer(file);
});

However, <input type="file"> requires user interaction, which is tedious.

I thought I might be able to work around the no-AJAX limitation by using a build tool to convert my database file to a JavaScript object / string and generate a ".js" file providing the file contents, and then convert the file contents to a Uint8Array, somehow.

Psuedocode:

// In Node.js:
var fs = require('fs');
var sqliteDb = fs.readFileSync('path/to/sqlite.db');
var string = convertBufferToJsStringSomehow(sqliteDb);
fs.writeFileSync('build/db.js', 'var dbString = "' + string + '";');
// In the browser (assume "build/db.js" has been loaded via a <script> tag):
var uint8Array = convertStringToUint8ArraySomehow(dbString);
var db = new sql.Database(uint8Array);

In Node.js, I've tried the following:

var TextEncoder = require('text-encoding').TextEncoder;
var TextDecoder = require('text-encoding').TextEncoder;
var sql = require('sql.js');

var string = new TextDecoder('utf-8').decode(fs.readFileSync('path/to/sqlite.db'));
// At this point, I would write `string` to a ".js" file, but for
// the sake of determining if converting back to a Uint8Array
// would work, I'll continue in Node.js...
var uint8array = new TextEncoder().encode(string);
var db = new sql.Database(uint8array);
db.exec('SELECT * FROM tablename');

But when I do that, I get the error "Error: database disk image is malformed".

What am I doing wrong? Is this even possible? The database disk image isn't "malformed" when I load the same file via FileReader.

Jackson
  • 9,188
  • 6
  • 52
  • 77
  • What is result of `fs.readFileSync('path/to/sqlite.db')`? – guest271314 Sep 26 '16 at 20:34
  • @guest271314 The result is ``. – Jackson Sep 26 '16 at 20:35
  • What is the result of `string`? – guest271314 Sep 26 '16 at 20:37
  • A gigantic string (`string.length` is `3202320`). – Jackson Sep 26 '16 at 20:39
  • Though `var uint8array = new TextEncoder().encode(string);` is not encoding string back to `Uint8Array` correctly? Note, you can create a `.js` file using `Blob` or `File` object by setting `type` to `"application/javascript"` see http://stackoverflow.com/questions/39315017/using-html-how-do-i-open-a-file-as-an-excel-file – guest271314 Sep 26 '16 at 20:40
  • why not specify 'utf8' to node's native `fs.readFileSync` call? Something like... `db = new sql.Database(Uint8Array.from(readFileSync('sqlite.db', {encoding: 'utf8'})))`. Seems like `text-encoding` module is completely unnecessary, no? – Mulan Sep 26 '16 at 20:40
  • @guest271314 `var uint8array = new TextEncoder().encode(string);` does not appear to encode the string to `Uint8Array` correctly, considering that I get "Error: database disk image is malformed" when querying the database. The question you linked also isn't applicable here; I am trying to generate a string representation of a binary file outside of a browser context. – Jackson Sep 26 '16 at 20:48
  • @naomik I tried your suggestion, but when I queried the database, I got the error, "Error: file is encrypted or is not a database" – Jackson Sep 26 '16 at 20:48
  • Not certain how browser context is related? Is `Blob` not defined at `nodejs`? _"I am trying to generate a string representation of a binary file"_ Are you trying to generate a string or `ArrayBuffer`? Have you tried using `FileReader` at `nodejs`? To convert `string` to `ArrayBuffer`? – guest271314 Sep 26 '16 at 20:50
  • @guest271314 I am trying to generate a string from a binary file, and then convert that string to a Uint8Array. `FileReader` is a browser API, it doesn't exist in Node.js. – Jackson Sep 26 '16 at 20:55
  • @Jackson https://www.npmjs.com/package/filereader , https://www.npmjs.com/package/file-reader . What is `MIME` type of file? – guest271314 Sep 26 '16 at 20:57
  • Can you provide a solution using `FileReader`? There is no MIME type. However, the file is a SQLite database file. – Jackson Sep 26 '16 at 21:00
  • @Jackson _"Can you provide a solution using `FileReader`? "_ If _"And that's working with the following code:"_ is correct, you should be able to use the same pattern as you have used at `javascript` at Question ? `var string = fs.readFileSync('path/to/sqlite.db'); // do nodejs FileReader stuff with string`? – guest271314 Sep 26 '16 at 21:03
  • I don't think so, because `FileReader` provides me with an `ArrayBuffer`, not a string. The result of `fs.readFileSync` was also not useful to me, see my attempt in the OP and my response to naomik's suggestion. – Jackson Sep 26 '16 at 21:09
  • Original Question describes passing `ArrayBuffer` to `sql.Database`, not string. What is purpose of converting `ArrayBuffer` to string then back to `ArrayBuffer`? If using `FileReader` returned expected result at `change` event, should return same result at `nodejs` – guest271314 Sep 26 '16 at 21:11

2 Answers2

3

Using the following code, I was able to transfer the database file's contents to the browser:

// In Node.js:
var fs = require('fs');
var base64 = fs.readFileSync('path/to/sqlite.db', 'base64');
fs.writeFileSync('build/db.js', 'var dbString = "' + base64 + '";');
// In the browser (assume "build/db.js" has been loaded via a <script> tag):
function base64ToUint8Array (string) {
  var raw = atob(string);
  var rawLength = raw.length;
  var array = new Uint8Array(new ArrayBuffer(rawLength));
  for (var i = 0; i < rawLength; i += 1) {
    array[i] = raw.charCodeAt(i);
  }
  return array;
}
var db = new sql.Database(base64ToUint8Array(dbString));
console.log(db.exec('SELECT * FROM tablename'));
Jackson
  • 9,188
  • 6
  • 52
  • 77
  • What do you mean by "more direct manner"? If your Answer resolves Question you can accept your own Answer. – guest271314 Sep 27 '16 at 04:57
  • By a "more direct manner" I mean "a manner other than using a data URL which has faster execution time". – Jackson Sep 27 '16 at 05:00
  • _"a manner other than using a data URL which has faster execution time"_ Do not believe that is possible. You could use a `Blob URL`, though that would not persist beyond browser session. – guest271314 Sep 27 '16 at 05:03
  • I was able to simplify the solution slightly by only using base64 encoding (without the data URL part). It seems base64 is a convenient format for sending binary data in text contexts, so it makes sense why it's working for me. – Jackson Sep 27 '16 at 07:10
  • You are able to achieve requirement without `FileReader` then, yes? And trim 10-20 characters from `data URI`? – guest271314 Sep 27 '16 at 07:14
1

And that's working with the following code:

input.addEventListener('change', function (changeEvent) {
  var file = changeEvent.currentTarget.files[0];
  var reader = new FileReader();
  reader.addEventListener('load', function (loadEvent) {
    var buffer = loadEvent.target.result;
    var uint8Array = new Uint8Array(buffer);
    var db = new sql.Database(uint8Array);
  });
  reader.readAsArrayBuffer(file);
});

However, <input type="file"> requires user interaction, which is tedious.

Using current working approach would be less tedious than attempting to create workarounds. If user intends to use application, user can select file from their filesystem to run application.

guest271314
  • 1
  • 15
  • 104
  • 177
  • Please re-read my question. I need to convert "sqlite.db" to a string so that I can send it to the browser in a ".js" file. – Jackson Sep 26 '16 at 21:11
  • Is _"And that's working with the following code:"_ correct? Not sure how or why result would be different using `FileReadrer` at `nodejs`? You can convert the string to a `.js` file using `Blob` or `File` objects, as demonstrated at http://stackoverflow.com/questions/39315017/using-html-how-do-i-open-a-file-as-an-excel-file. – guest271314 Sep 26 '16 at 21:14
  • If you think that will work, please demonstrate it in your answer. Also, I added an example of my use case to the OP, see the code under "Psuedocode:". – Jackson Sep 26 '16 at 21:17
  • Demonstrate what? Converting a string to a `.js` file? Or convert string to `Uint8Array`? – guest271314 Sep 26 '16 at 21:18
  • I would like to see exactly what code is required is to convert the contents of the file "sqlite.db" to a JS string, where that JS string can be delivered to a browser via a ".js" file, and then be converted to a `Uint8Array` in a browser. – Jackson Sep 26 '16 at 21:21
  • So a browser is involved? What is purpose of setting type of file to `.js` if it is not `.js`? You can load a ` – guest271314 Sep 26 '16 at 21:25
  • Some browsers forbid loading non-JS via ` – Jackson Sep 26 '16 at 21:29
  • @Jackson Do not have time at this moment. Though will when can. If you have control of `html` document. Instead of setting or including `src` of `script` element to `sqlite.db`, `fetch` URL, then set the `.textContent` of ` – guest271314 Sep 26 '16 at 21:35
  • @Jackson _"However, `` requires user interaction, which is tedious."_ The approach using `` with `change`, `drop` events attached is less tedious than workarounds. Not certain if `.db` charcter encoding is `UTF-8` http://kunststube.net/encoding/ ; see also http://stackoverflow.com/questions/11303405/force-encode-from-us-ascii-to-utf-8-iconv – guest271314 Sep 27 '16 at 00:22
  • I disagree that requiring user interaction is less tedious, so I am unlikely to accept this as an answer. – Jackson Sep 27 '16 at 03:23
  • @Jackson _"I disagree that requiring user interaction is less tedious"_ Well, you have working code. Not that it matters, though invested a fair amount of time trying a number of approaches to achieve this without user action; including using an `iframe` at `file:` protocol; changing `.db` file type to `.txt`. That was far more tedious than using existing working process. Why would you not want user click or drop at `` to affirmatively provide a file from their filesystem? What is tedious about that brief process? Are you trying to read user file without user being aware of the read? – guest271314 Sep 27 '16 at 03:28
  • It's likely that they'll always be selecting the same database file, so it'd be nicer if that file could be loaded automatically. In my case, it'd be like starting up Microsoft Word, and having to locate the English Dictionary on your disk before you could use spellcheck. – Jackson Sep 27 '16 at 03:35
  • Have you seen the edit to the OP from earlier? I think we just need to figure out how to implement the functions `convertBufferToJsStringSomehow` and `convertStringToUint8ArraySomehow`. – Jackson Sep 27 '16 at 03:38
  • That is part of procedure to have a layer of "security" at user filesystem. For good reason. Users make mistakes. A text editor does have to locate the files which run specific portions of programs. Rarely does a program consist of a single file. What you could alternatively do is use existing approach once, then store the resulting data at `localStorage`; or at chrome, chromium, `LocalFileSystem` using `requestFileSystem`. Then retrieve the file at subsequent page loads from `localStorage` or `LocalFileSystem` – guest271314 Sep 27 '16 at 03:39
  • `localStorage` stores data as strings. So I would still need to know how to convert a buffer to a JS string and back again. – Jackson Sep 27 '16 at 03:41
  • _"`localStorage` stores data as strings. So I would still need to know how to convert a buffer to a JS string and back again."_ You can store the data as `JSON` at `localStorage`, e.g., `"[0,1]"`, then use `JSON.parse()` to parse `JSON`, `JSON.stringify()` to convert back to string. _"I think we just need to figure out how to implement the functions convertBufferToJsStringSomehow and convertStringToUint8ArraySomehow"_ Have you tried using `FileReader` at `nodejs`? – guest271314 Sep 27 '16 at 03:44
  • Furthermore, I can't store this database using `localStorage` because it is over 3MB (I get `QuotaExceededError`). I don't know how to use `FileReader` to obtain a JS string which can be converted to a `Uint8Array`. – Jackson Sep 27 '16 at 03:52
  • _"I don't know how to use `FileReader` to obtain a JS string which can be converted to a `Uint8Array`"_ What do you mean? If you are able to retrieve the file using `nodejs` you should be able to set the response type to `ArrayBuffer` itself, or `Blob`, then use `FileReader`. _"Furthermore, I can't store this database using `localStorage` because it is over 3MB (I get `QuotaExceededError`)."_ You should be able to use `requestFileSystem` at chrome, chromium. Again, the issues with possible workarounds is, as appears here, more tedious than simply selecting the file at click or dropping file – guest271314 Sep 27 '16 at 03:59
  • That's nice, but I still don't know what code will turn a file into a JS string that can then be converted to a `Uint8Array`. I'd appreciate if you could share the exact code which does that. I tried many approaches, but failed. That's why I asked this question. I was hoping someone else would know precisely which methods to call to accomplish the task. I understand what needs to be done at a high level, but as far as what specifically needs to be done, I am at a loss. – Jackson Sep 27 '16 at 04:06
  • _"but I still don't know what code will turn a file into a JS string that can then be converted to a `Uint8Array`"_ You are already using that code. `FileReader` `.readAsArrayBuffer` returns `ArrayBuffer` _"And that's working with the following code:"_ Does your working code actually return expected result? – guest271314 Sep 27 '16 at 04:11
  • Why is the conversion to string needed in the process? – guest271314 Sep 27 '16 at 04:16
  • _"but I still don't know what code will turn a file into a JS string that can then be converted to a `Uint8Array`"_ See http://stackoverflow.com/q/9267899/ – guest271314 Sep 27 '16 at 04:25
  • Using `FileReader.prototype.readAsArrayBuffer` in Node.js doesn't work: http://pastebin.com/FnPTWNAa When "db.js" is loaded in the browser, I get "Uncaught SyntaxError: Unexpected identifier". – Jackson Sep 27 '16 at 04:31
  • You are trying to concatenate an `ArrayBuffer` to a string, instead of converting the `ArrayBuffer` to string first. If the string is _"over 3MB"_ why are you trying to create a `javascript` string variable that is over 3MB? You could try using `.readAsDataURL()` to return a string from `FileReader`. Though if the string is 3MB, that could cause other issues. – guest271314 Sep 27 '16 at 04:35
  • Thanks, I had some success with `readAsDataURL` (which I posted as an answer), although it's pretty slow. I wonder why I could not use some other method to turn a file into a string. A data URL kinda seems like a random format to pick. – Jackson Sep 27 '16 at 04:57
  • How else would you represent file as a string that is persistent? – guest271314 Sep 27 '16 at 04:59
  • "How else would you represent file as a string that is persistent?" I don't know. – Jackson Sep 27 '16 at 05:01
  • See also http://stackoverflow.com/questions/39334494/converting-large-numbers-from-binary-to-decimal-and-back-in-javascript – guest271314 Sep 27 '16 at 05:05