Skip to content

Commit

Permalink
Add FileSystem API to read/write files and use it (#3977)
Browse files Browse the repository at this point in the history
* Add FileSystem API to read/write files and use it

* Fixed issues as per review
  • Loading branch information
McGiverGim authored May 23, 2024
1 parent 3fe659e commit 4823832
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 563 deletions.
5 changes: 4 additions & 1 deletion locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@
"logActionShow": {
"message": "Show Log"
},

"fileSystemPickerFiles": {
"message": "{{typeof}} files",
"description": "Text for the file picker dialog, showing the type of files selected. The parameter can be HEX, TXT, etc."
},
"serialErrorFrameError": {
"message": "Serial connection error: bad framing"
},
Expand Down
104 changes: 104 additions & 0 deletions src/js/FileSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

class FileSystem {

_createFile(fileHandle) {
return {
name: fileHandle.name,
_fileHandle: fileHandle,
};
}

async pickSaveFile(suggestedName, description, extension) {
const fileHandle = await window.showSaveFilePicker({
suggestedName: suggestedName,
types: [{
description: description,
accept: {
"application/unknown": extension,
},
}],
});

const file = this._createFile(fileHandle);

if (await this.verifyPermission(file, true)) {
return file;
}
}

async pickOpenFile(description, extension) {
const fileHandle = await window.showOpenFilePicker({
multiple: false,
types: [{
description: description,
accept: {
"application/unknown": extension,
},
}],
});

const file = this._createFile(fileHandle[0]);

if (await this.verifyPermission(file, false)) {
return file;
}
}

async verifyPermission(file, withWrite) {
const fileHandle = file._fileHandle;

const opts = {};
opts.mode = withWrite ? "readwrite" : "read";

if ((await fileHandle.queryPermission(opts)) === "granted") {
console.log("The user has %s permissions for the file: %s", opts.mode, fileHandle.name);
return true;
}

if ((await fileHandle.requestPermission(opts)) === "granted") {
console.log("Request %s permissions for the file: %s", opts.mode, fileHandle.name);
return true;
}

console.error("The user has no permission for file: ", fileHandle.name);
throw new Error("The user has no %s permission for file: %s", opts.mode, fileHandle.name);
}

async writeFile(file, contents) {
const fileHandle = file._fileHandle;

const writable = await fileHandle.createWritable();
await writable.write(contents);
await writable.close();
}


async readFile(file) {
const fileHandle = file._fileHandle;

const fileReader = await fileHandle.getFile();
return await fileReader.text();
}

async readFileAsBlob(file) {
const fileHandle = file._fileHandle;

return await fileHandle.getFile();
}

async openFile(file) {
const fileHandle = file._fileHandle;

return await fileHandle.createWritable();
}

async writeChunck(writable, chunk) {
return await writable.write(chunk);
}

async closeFile(writable) {
return await writable.close();
}
}

export default new FileSystem();
39 changes: 21 additions & 18 deletions src/js/LogoManager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { gui_log } from "./gui_log";
import { i18n } from "./localization";
import { checkChromeRuntimeError } from "./utils/common";
import $ from 'jquery';
import FileSystem from "./FileSystem";

/**
* Takes an ImageData object and returns an MCM symbol as an array of strings.
Expand Down Expand Up @@ -229,23 +229,26 @@ LogoManager.hideUploadHint = function () {
*/
LogoManager.openImage = function () {
return new Promise((resolveOpenImage, rejectOpenImage) => {
const dialogOptions = {
type: 'openFile',
accepts: this.acceptFileTypes,
};
chrome.fileSystem.chooseEntry(dialogOptions, fileEntry => {
if (checkChromeRuntimeError()) {
return;
}
// load and validate selected image
const img = new Image();
img.onload = () => {
validateImage.apply(this, [img])
.then(() => resolveOpenImage(img))
.catch(error => rejectOpenImage(error));
};
img.onerror = error => rejectOpenImage(error);
fileEntry.file(file => img.src = `file://${file.path}`);
//FileSystem.pickOpenFile(i18n.getMessage('fileSystemPickerFiles', {typeof: this.acceptFileTypes[0].description.toUpperCase()}), `.${this.acceptFileTypes[0].extensions.join('.,')}`)
FileSystem.pickOpenFile(i18n.getMessage('fileSystemPickerFiles', {typeof: this.acceptFileTypes[0].description.toUpperCase()}), this.acceptFileTypes[0].extensions.map((ext)=> {return `.${ext}`;}))
.then((file) => {
FileSystem.readFileAsBlob(file)
.then((data) => {
// load and validate selected image
const img = new Image();
img.onload = () => {
validateImage.apply(this, [img])
.then(() => resolveOpenImage(img))
.catch(error => rejectOpenImage(error));
};
img.onerror = error => rejectOpenImage(error);

const blobUrl = URL.createObjectURL(data);
img.src = blobUrl;
});
})
.catch((error) => {
console.error('could not load logo file:', error);
});
});
};
Expand Down
170 changes: 44 additions & 126 deletions src/js/backup_restore.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { gui_log } from './gui_log';
import { generateFilename } from "./utils/generate_filename";
import semver from "semver";
import { tracking } from "./Analytics";
import { checkChromeRuntimeError } from "./utils/common";
import FileSystem from "./FileSystem.js";

// code below is highly experimental, although it runs fine on latest firmware
// the data inside nested objects needs to be verified if deep copy works properly
Expand Down Expand Up @@ -199,69 +199,18 @@ export function configuration_backup(callback) {

const filename = generateFilename(prefix, suffix);

const accepts = [{
description: `${suffix.toUpperCase()} files`, extensions: [suffix],
}];

// create or load the file
chrome.fileSystem.chooseEntry({type: 'saveFile', suggestedName: filename, accepts: accepts}, function (fileEntry) {
if (checkChromeRuntimeError()) {
return;
}
FileSystem.pickSaveFile(filename, i18n.getMessage('fileSystemPickerFiles', {typeof: suffix.toUpperCase()}), `.${suffix}`)
.then((file) => {
const serializedConfigObject = JSON.stringify(configuration, null, '\t');

if (!fileEntry) {
console.log('No file selected, backup aborted.');
return;
}
console.log("Saving backup to:", file.name);
FileSystem.writeFile(file, serializedConfigObject);

chosenFileEntry = fileEntry;

// echo/console log path specified
chrome.fileSystem.getDisplayPath(chosenFileEntry, function (path) {
console.log(`Backup file path: ${path}`);
});

// change file entry from read only to read/write
chrome.fileSystem.getWritableEntry(chosenFileEntry, function (fileEntryWritable) {
// check if file is writable
chrome.fileSystem.isWritableEntry(fileEntryWritable, function (isWritable) {
if (isWritable) {
chosenFileEntry = fileEntryWritable;

// crunch the config object
const serializedConfigObject = JSON.stringify(configuration, null, '\t');
const blob = new Blob([serializedConfigObject], {type: 'text/plain'}); // first parameter for Blob needs to be an array

chosenFileEntry.createWriter(function (writer) {
writer.onerror = function (e) {
console.error(e);
};

let truncated = false;
writer.onwriteend = function () {
if (!truncated) {
// onwriteend will be fired again when truncation is finished
truncated = true;
writer.truncate(blob.size);

return;
}

tracking.sendEvent(tracking.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Backup');
console.log('Write SUCCESSFUL');
if (callback) callback();
};

writer.write(blob);
}, function (e) {
console.error(e);
});
} else {
// Something went wrong or file is set to read only and cannot be changed
console.log('File appears to be read only, sorry.');
}
});
});
tracking.sendEvent(tracking.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Backup');
})
.catch((error) => {
console.log('Error in backup process: ', error);
});
}

Expand All @@ -270,77 +219,46 @@ export function configuration_backup(callback) {
export function configuration_restore(callback) {
let chosenFileEntry = null;

const accepts = [{
description: 'JSON files', extensions: ['json'],
}];

// load up the file
chrome.fileSystem.chooseEntry({type: 'openFile', accepts: accepts}, function (fileEntry) {
if (checkChromeRuntimeError()) {
return;
}

if (!fileEntry) {
console.log('No file selected, restore aborted.');
return;
}

chosenFileEntry = fileEntry;
const suffix = 'json';

FileSystem.pickOpenFile(i18n.getMessage('fileSystemPickerFiles', {typeof: suffix.toUpperCase()}), `.${suffix}`)
.then((file) => {
console.log("Reading VTX config from:", file.name);
FileSystem.readFile(file)
.then((text) => {
console.log('Read backup SUCCESSFUL');
let configuration;
try { // check if string provided is a valid JSON
configuration = JSON.parse(text);
} catch (err) {
// data provided != valid json object
console.log(`Data provided != valid JSON string, restore aborted: ${err}`);

// echo/console log path specified
chrome.fileSystem.getDisplayPath(chosenFileEntry, function (path) {
console.log(`Restore file path: ${path}`);
});

// read contents into variable
chosenFileEntry.file(function (file) {
const reader = new FileReader();
return;
}

reader.onprogress = function (e) {
if (e.total > 1048576) { // 1 MB
// dont allow reading files bigger then 1 MB
console.log('File limit (1 MB) exceeded, aborting');
reader.abort();
// validate
if (typeof configuration.generatedBy !== 'undefined' && compareVersions(configuration.generatedBy, CONFIGURATOR.BACKUP_FILE_VERSION_MIN_SUPPORTED)) {
if (!compareVersions(configuration.generatedBy, "1.14.0") && !migrate(configuration)) {
gui_log(i18n.getMessage('backupFileUnmigratable'));
return;
}
};

reader.onloadend = function (e) {
if ((e.total != 0 && e.total == e.loaded) || GUI.isCordova()) {
// Cordova: Ignore verification : seem to have a bug with progressEvent returned
console.log('Read SUCCESSFUL');
let configuration;
try { // check if string provided is a valid JSON
configuration = JSON.parse(e.target.result);
} catch (err) {
// data provided != valid json object
console.log(`Data provided != valid JSON string, restore aborted: ${err}`);

return;
}

// validate
if (typeof configuration.generatedBy !== 'undefined' && compareVersions(configuration.generatedBy, CONFIGURATOR.BACKUP_FILE_VERSION_MIN_SUPPORTED)) {
if (!compareVersions(configuration.generatedBy, "1.14.0") && !migrate(configuration)) {
gui_log(i18n.getMessage('backupFileUnmigratable'));
return;
}
if (configuration.FEATURE_CONFIG.features._featureMask) {
const features = new Features(FC.CONFIG);
features.setMask(configuration.FEATURE_CONFIG.features._featureMask);
configuration.FEATURE_CONFIG.features = features;
}

tracking.sendEvent(tracking.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Restore');

configuration_upload(configuration, callback);
} else {
gui_log(i18n.getMessage('backupFileIncompatible'));
}
if (configuration.FEATURE_CONFIG.features._featureMask) {
const features = new Features(FC.CONFIG);
features.setMask(configuration.FEATURE_CONFIG.features._featureMask);
configuration.FEATURE_CONFIG.features = features;
}
};

reader.readAsText(file);
tracking.sendEvent(tracking.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Restore');

configuration_upload(configuration, callback);
} else {
gui_log(i18n.getMessage('backupFileIncompatible'));
}
});
})
.catch((error) => {
console.log('Error in restore process: ', error);
});

function compareVersions(generated, required) {
Expand Down
Loading

0 comments on commit 4823832

Please sign in to comment.