[HTML5] Opening FileSystem-Files in WebGL Export (maybe with JavaScript workaround?)

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By JSchmalhofer

Hello,

I am writing a multi-OS 3D Visualization Tool in Godot. Essentially, I am loading an XML-File from FileSystem, parsing it, and 3D-Visualizing relations in it.

This works great on Windows and Linux. I’d also have this running in Browser as HTML5 export. However, the FileSystem-access there is limited to a sandbox, i.e. the user cannot access his XML-files he wants to visualize.

I have found a related issue where the JavaScript-Singleton was extended by some easy download-feature. Is it possible to have something like that for “upload” so the user can upload (text-based) files from his FileSystem?

Edit: I also tried approaches like calling the JS FS-Read as one-liner, unfortunately that does not seem to work:

var textcontent = JavaScript.eval("const fs = require('fs');const buffer = fs.readFileSync('file:///C:/Work/infile.txt'); buffer.toString();")

Any help is appreciated! Thanks!

:bust_in_silhouette: Reply From: exuin

Check out this which allows the user to upload images. Presumably you can alter the “accept” attribute to only allow xml files?

@exuin : Thank you very much, that actually solved my problem.

I had to make following changes:

  • add my file-type (e.g. “.xml” as accepted file type in HTML5FileExchange.gd =
  • make the JavaScript reader read the file as text using readAsText() instead of readAsArrayBuffer()
  • emit the content of file being read instead of doing some image conversion (unfortunately, I did not find a way to distinguish different file types for now, somehow no filetype is being returned as of now)

I only need the upload-functionality, so I removed the download function. My scripts now look like this:

res://addons/HTML5FileExchange/HTML5FileExchange.gd

extends Node

signal read_completed
signal load_completed(file)

var js_callback = JavaScript.create_callback(self, "load_handler");
var js_interface;

func _ready():
	if OS.get_name() == "HTML5" and OS.has_feature('JavaScript'):
		_define_js()
		js_interface = JavaScript.get_interface("_HTML5FileExchange");

func _define_js()->void:
	#Define JS script
	JavaScript.eval("""
	var _HTML5FileExchange = {};
	_HTML5FileExchange.upload = function(gd_callback) {
		canceled = true;
		var input = document.createElement('INPUT'); 
		input.setAttribute("type", "file");
		input.setAttribute("accept", ".xodr");
		input.click();
		input.addEventListener('change', event => {
			if (event.target.files.length > 0){
				canceled = false;}
			var file = event.target.files[0];
			var reader = new FileReader();
			this.fileType = file.type;
			// var fileName = file.name;
			reader.readAsText(file);
			reader.onloadend = (evt) => { // Since here's it's arrow function, "this" still refers to _HTML5FileExchange
				if (evt.target.readyState == FileReader.DONE) {
					this.result = evt.target.result;
					gd_callback(); // It's hard to retrieve value from callback argument, so it's just for notification
				}
			}
		  });
	}
	""", true)

func load_handler(_args):
	emit_signal("read_completed")
	
func load_file():
	if OS.get_name() != "HTML5" or !OS.has_feature('JavaScript'):
		return

	js_interface.upload(js_callback);

	yield(self, "read_completed")
	
	var fileType = js_interface.fileType;
	var fileContent = JavaScript.eval("_HTML5FileExchange.result", true) # interface doesn't work as expected for some reason
	
	emit_signal("load_completed", fileContent)

and the GDScript uses it like this:

func _on_UploadButton_pressed():
	HTML5File.load_file()

	var content = yield(HTML5File, "load_completed")
	
	get_node("/root/Control/RichTextLabel").text = content

JSchmalhofer | 2022-05-29 15:12