File uploads now support up to 1GB files, and display progress
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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+",
|
||||||
|
|||||||
@@ -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 () {
|
||||||
@@ -530,7 +525,7 @@ module.exports = {
|
|||||||
this.send(JSON.stringify(data));
|
this.send(JSON.stringify(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
showProbeDialog: function(probeType) {
|
showProbeDialog: function (probeType) {
|
||||||
SvelteComponents.showDialog("Probe", { probeType });
|
SvelteComponents.showDialog("Probe", { probeType });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
74
src/svelte-components/src/dialogs/UploadDialog.svelte
Normal file
74
src/svelte-components/src/dialogs/UploadDialog.svelte
Normal 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>
|
||||||
Reference in New Issue
Block a user