File uploads now support up to 1GB files, and display progress

This commit is contained in:
David Carley
2022-07-19 20:30:06 -07:00
parent 6f2f6a306b
commit 331a5ea1b8
6 changed files with 145 additions and 61 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "bbctrl", "name": "bbctrl",
"version": "1.0.10b4", "version": "1.0.10b5",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bbctrl", "name": "bbctrl",
"version": "1.0.10b4", "version": "1.0.10b5",
"license": "GPL-3.0+", "license": "GPL-3.0+",
"dependencies": { "dependencies": {
"browserify": "^17.0.0", "browserify": "^17.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "bbctrl", "name": "bbctrl",
"version": "1.0.10b4", "version": "1.0.10b5",
"homepage": "https://onefinitycnc.com/", "homepage": "https://onefinitycnc.com/",
"repository": "https://github.com/OneFinityCNC/onefinity", "repository": "https://github.com/OneFinityCNC/onefinity",
"license": "GPL-3.0+", "license": "GPL-3.0+",

View File

@@ -398,18 +398,13 @@ module.exports = {
return; return;
} }
const fd = new FormData(); SvelteComponents.showDialog("Upload", {
file,
fd.append('gcode', file); onComplete: () => {
try {
await api.upload('file', fd);
this.last_file_time = undefined; // Force reload this.last_file_time = undefined; // Force reload
this.$broadcast('gcode-reload', file.name); this.$broadcast('gcode-reload', file.name);
} catch (err) {
api.alert('Upload failed', err)
} }
});
}, },
delete_current: function () { delete_current: function () {

View File

@@ -1,34 +1,8 @@
################################################################################
# #
# This file is part of the Buildbotics firmware. #
# #
# Copyright (c) 2015 - 2018, Buildbotics LLC #
# All rights reserved. #
# #
# This file ("the software") is free software: you can redistribute it #
# and/or modify it under the terms of the GNU General Public License, #
# version 2 as published by the Free Software Foundation. You should #
# have received a copy of the GNU General Public License, version 2 #
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
# #
# The software is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
# Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public #
# License along with the software. If not, see #
# <http://www.gnu.org/licenses/>. #
# #
# For information regarding this software email: #
# "Joseph Coffland" <joseph@buildbotics.com> #
# #
################################################################################
import os import os
import tempfile
import bbctrl import bbctrl
import glob import glob
import html import tornado
from tornado import gen from tornado import gen
from tornado.web import HTTPError from tornado.web import HTTPError
@@ -36,17 +10,32 @@ from tornado.web import HTTPError
def safe_remove(path): def safe_remove(path):
try: try:
os.unlink(path) os.unlink(path)
except OSError: pass except OSError:
pass
@tornado.web.stream_request_body
class FileHandler(bbctrl.APIHandler): class FileHandler(bbctrl.APIHandler):
def prepare(self): pass def prepare(self):
if self.request.method == 'PUT':
self.request.connection.set_max_body_size(2 ** 30)
self.uploadFilename = self.request.path.split('/')[-1] \
.replace('\\', '/') \
.replace('#', '-') \
.replace('?', '-')
self.uploadFile = tempfile.NamedTemporaryFile("wb")
def data_received(self, data):
if self.request.method == 'PUT':
self.uploadFile.write(data)
def delete_ok(self, filename): def delete_ok(self, filename):
if not filename: if not filename:
# Delete everything # Delete everything
for path in glob.glob(self.get_upload('*')): safe_remove(path) for path in glob.glob(self.get_upload('*')):
safe_remove(path)
self.get_ctrl().preplanner.delete_all_plans() self.get_ctrl().preplanner.delete_all_plans()
self.get_ctrl().state.clear_files() self.get_ctrl().state.clear_files()
@@ -57,26 +46,29 @@ class FileHandler(bbctrl.APIHandler):
self.get_ctrl().preplanner.delete_plans(filename) self.get_ctrl().preplanner.delete_plans(filename)
self.get_ctrl().state.remove_file(filename) self.get_ctrl().state.remove_file(filename)
def put_ok(self, *args): def put_ok(self, *args):
gcode = self.request.files['gcode'][0] if not os.path.exists(self.get_upload()):
filename = os.path.basename(gcode['filename'].replace('\\', '/')) os.mkdir(self.get_upload())
filename = filename.replace('#', '-').replace('?', '-')
if not os.path.exists(self.get_upload()): os.mkdir(self.get_upload()) filename = self.get_upload(self.uploadFilename).encode('utf8')
safe_remove(filename)
os.link(self.uploadFile.name, filename)
with open(self.get_upload(filename).encode('utf8'), 'wb') as f: self.uploadFile.close()
f.write(gcode['body'])
os.sync()
self.get_ctrl().preplanner.invalidate(filename) del(self.uploadFile)
self.get_ctrl().state.add_file(filename)
self.get_log('FileHandler').info('GCode received: ' + filename)
self.get_ctrl().preplanner.invalidate(self.uploadFilename)
self.get_ctrl().state.add_file(self.uploadFilename)
self.get_log('FileHandler').info(
'GCode received: ' + self.uploadFilename)
del(self.uploadFilename)
@gen.coroutine @gen.coroutine
def get(self, filename): def get(self, filename):
if not filename: raise HTTPError(400, 'Missing filename') if not filename:
raise HTTPError(400, 'Missing filename')
filename = os.path.basename(filename) filename = os.path.basename(filename)
try: try:
@@ -84,6 +76,7 @@ class FileHandler(bbctrl.APIHandler):
self.write(f.read()) self.write(f.read())
except Exception: except Exception:
self.get_ctrl().state.select_file('') self.get_ctrl().state.select_file('')
raise HTTPError(400, "Unable to read file - doesn't appear to be GCode.") raise HTTPError(
400, "Unable to read file - doesn't appear to be GCode.")
self.get_ctrl().state.select_file(filename) self.get_ctrl().state.select_file(filename)

View File

@@ -3,6 +3,7 @@
import HomeMachineDialog from "$dialogs/HomeMachineDialog.svelte"; import HomeMachineDialog from "$dialogs/HomeMachineDialog.svelte";
import ProbeDialog from "$dialogs/ProbeDialog.svelte"; import ProbeDialog from "$dialogs/ProbeDialog.svelte";
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte"; import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
import UploadDialog from "$dialogs/UploadDialog.svelte";
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>(); const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
type HomeMachineDialogPropsType = { type HomeMachineDialogPropsType = {
@@ -16,11 +17,17 @@
probeType: "xyz" | "z"; probeType: "xyz" | "z";
}; };
const ScreenRotationDialogProps = writable<ProbeDialogPropsType>(); const ScreenRotationDialogProps = writable<ScreenRotationDialogPropsType>();
type ScreenRotationDialogPropsType = { type ScreenRotationDialogPropsType = {
open: boolean; open: boolean;
}; };
const UploadDialogProps = writable<UploadDialogPropsType>();
type UploadDialogPropsType = {
open: boolean;
file: File;
};
export function showDialog( export function showDialog(
dialog: "HomeMachine", dialog: "HomeMachine",
props: Omit<HomeMachineDialogPropsType, "open"> props: Omit<HomeMachineDialogPropsType, "open">
@@ -31,6 +38,16 @@
props: Omit<ProbeDialogPropsType, "open"> props: Omit<ProbeDialogPropsType, "open">
); );
export function showDialog(
dialog: "ScreenRotation",
props: Omit<ScreenRotationDialogPropsType, "open">
);
export function showDialog(
dialog: "Upload",
props: Omit<UploadDialogPropsType, "open">
);
export function showDialog(dialog: string, props: any) { export function showDialog(dialog: string, props: any) {
switch (dialog) { switch (dialog) {
case "HomeMachine": case "HomeMachine":
@@ -45,6 +62,10 @@
ScreenRotationDialogProps.set({ ...props, open: true }); ScreenRotationDialogProps.set({ ...props, open: true });
break; break;
case "Upload":
UploadDialogProps.set({ ...props, open: true });
break;
default: default:
throw new Error(`Unknown dialog '${dialog}`); throw new Error(`Unknown dialog '${dialog}`);
} }
@@ -54,3 +75,4 @@
<HomeMachineDialog {...$HomeMachineDialogProps} /> <HomeMachineDialog {...$HomeMachineDialogProps} />
<ProbeDialog {...$ProbeDialogProps} /> <ProbeDialog {...$ProbeDialogProps} />
<ScreenRotationDialog {...$ScreenRotationDialogProps} /> <ScreenRotationDialog {...$ScreenRotationDialogProps} />
<UploadDialog {...$UploadDialogProps} />

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import LinearProgress from "@smui/linear-progress";
export let open = false;
export let file: File;
let wasOpen = false;
let xhr;
let progress;
$: if (open != wasOpen) {
if (!wasOpen) {
beginUpload();
}
wasOpen = open;
}
$: if (!open) {
xhr = undefined;
}
async function beginUpload() {
progress = 0;
xhr = new XMLHttpRequest();
xhr.upload.onload = () => {
open = false;
};
xhr.upload.onerror = () => {
open = false;
alert("Upload failed.");
};
xhr.upload.onabort = () => {
open = false;
};
xhr.upload.onprogress = (event) => {
progress = event.loaded / event.total;
};
xhr.open("PUT", `/api/file/${encodeURIComponent(file.name)}`);
xhr.send(file);
}
function onCancel() {
xhr.abort();
}
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="upload-dialog-title"
aria-describedby="upload-dialog-content"
>
<Title id="upload-dialog-title">
Uploading {#if file}{file.name}...{/if}
</Title>
<Content id="upload-dialog-content">
<LinearProgress {progress} />
</Content>
<Actions>
<Button on:click={onCancel}>
<Label>Cancel</Label>
</Button>
</Actions>
</Dialog>