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",
"version": "1.0.10b4",
"version": "1.0.10b5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bbctrl",
"version": "1.0.10b4",
"version": "1.0.10b5",
"license": "GPL-3.0+",
"dependencies": {
"browserify": "^17.0.0",

View File

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

View File

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

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 tempfile
import bbctrl
import glob
import html
import tornado
from tornado import gen
from tornado.web import HTTPError
@@ -36,17 +10,32 @@ from tornado.web import HTTPError
def safe_remove(path):
try:
os.unlink(path)
except OSError: pass
except OSError:
pass
@tornado.web.stream_request_body
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):
if not filename:
# 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().state.clear_files()
@@ -57,26 +46,29 @@ class FileHandler(bbctrl.APIHandler):
self.get_ctrl().preplanner.delete_plans(filename)
self.get_ctrl().state.remove_file(filename)
def put_ok(self, *args):
gcode = self.request.files['gcode'][0]
filename = os.path.basename(gcode['filename'].replace('\\', '/'))
filename = filename.replace('#', '-').replace('?', '-')
if not os.path.exists(self.get_upload()):
os.mkdir(self.get_upload())
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:
f.write(gcode['body'])
os.sync()
self.uploadFile.close()
self.get_ctrl().preplanner.invalidate(filename)
self.get_ctrl().state.add_file(filename)
self.get_log('FileHandler').info('GCode received: ' + filename)
del(self.uploadFile)
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
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)
try:
@@ -84,6 +76,7 @@ class FileHandler(bbctrl.APIHandler):
self.write(f.read())
except Exception:
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)

View File

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