Merge branch 'file-upload' into mbl-next
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m43s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m43s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
This commit is contained in:
@@ -153,6 +153,8 @@
|
|||||||
<li><button onclick="showScreen('playlist')">Open Playlist Editor</button></li>
|
<li><button onclick="showScreen('playlist')">Open Playlist Editor</button></li>
|
||||||
<!-- More screens can be added later -->
|
<!-- More screens can be added later -->
|
||||||
</ul>
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<button onclick="requestReboot()">Reboot to bootloader</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CONFIG EDITOR SCREEN -->
|
<!-- CONFIG EDITOR SCREEN -->
|
||||||
@@ -220,6 +222,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-horizontal">
|
<div class="flex-horizontal">
|
||||||
|
<button id="playlist-edit-back">Cancel</button>
|
||||||
<button id="playlist-edit-removetrack">Remove track(s)</button>
|
<button id="playlist-edit-removetrack">Remove track(s)</button>
|
||||||
<button id="playlist-edit-addtrack">Add track(s)</button>
|
<button id="playlist-edit-addtrack">Add track(s)</button>
|
||||||
<button id="playlist-edit-save">Save</button>
|
<button id="playlist-edit-save">Save</button>
|
||||||
@@ -235,42 +238,23 @@
|
|||||||
<div class="scroll-container">
|
<div class="scroll-container">
|
||||||
<div class="tree" id="playlist-filebrowser-tree">
|
<div class="tree" id="playlist-filebrowser-tree">
|
||||||
<ul><li>Loading...</li></ul>
|
<ul><li>Loading...</li></ul>
|
||||||
<!--<ul>
|
|
||||||
<li>
|
|
||||||
<span class="caret"></span>
|
|
||||||
<span class="node">Fruits</span>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<span class="caret"></span>
|
|
||||||
<span class="node">Apple</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="caret"></span>
|
|
||||||
<span class="node">Citrus</span>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<span class="caret"></span>
|
|
||||||
<span class="node">Orange</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="caret"></span>
|
|
||||||
<span class="node">Lemon</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span class="caret"></span>
|
|
||||||
<span class="node">Strawberry</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-horizontal">
|
<div class="flex-horizontal">
|
||||||
<button id="playlist-filebrowser-cancel">Cancel</button>
|
<button id="playlist-filebrowser-cancel">Cancel</button>
|
||||||
|
<button id="playlist-filebrowser-delete" style="background-color: orangered">Delete selected</button>
|
||||||
<button id="playlist-filebrowser-addtrack">Add track(s)</button>
|
<button id="playlist-filebrowser-addtrack">Add track(s)</button>
|
||||||
</div>
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="flex-horizontal">
|
||||||
|
<input type="file" id="playlist-filebrowser-upload-files" multiple accept="audio/mpeg" />
|
||||||
|
<progress id="playlist-filebrowser-upload-progress" max="100" value="0" style:"flex-grow: 1">0%</progress>
|
||||||
|
<button id="playlist-filebrowser-upload">Upload</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-horizontal">
|
||||||
|
<input type="text" id="playlist-filebrowser-mkdir-name" placeholder="Directory Name" />
|
||||||
|
<button id="playlist-filebrowser-mkdir" style="width: 20%">Create directory</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -482,6 +466,7 @@
|
|||||||
document.getElementById('playlist-exist-button-gettag')
|
document.getElementById('playlist-exist-button-gettag')
|
||||||
.addEventListener('click', selectLastTag);
|
.addEventListener('click', selectLastTag);
|
||||||
document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => {
|
document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => {
|
||||||
|
if (lastSelected !== null)
|
||||||
showScreen("playlist_edit", {load: lastSelected.innerText});
|
showScreen("playlist_edit", {load: lastSelected.innerText});
|
||||||
});
|
});
|
||||||
document.getElementById('playlist-exist-list').addEventListener("click", (e) => {
|
document.getElementById('playlist-exist-list').addEventListener("click", (e) => {
|
||||||
@@ -558,12 +543,14 @@
|
|||||||
|
|
||||||
function selectTag(tag) {
|
function selectTag(tag) {
|
||||||
const container = document.getElementById('playlist-exist-list');
|
const container = document.getElementById('playlist-exist-list');
|
||||||
|
if (tag.tag === null) return;
|
||||||
const tagtext = bytesToHex(tag.tag);
|
const tagtext = bytesToHex(tag.tag);
|
||||||
document.getElementById('playlist-exist-list')
|
document.getElementById('playlist-exist-list')
|
||||||
.querySelectorAll("li")
|
.querySelectorAll("li")
|
||||||
.forEach(n => {
|
.forEach(n => {
|
||||||
if (n.innerText == tagtext) {
|
if (n.innerText == tagtext) {
|
||||||
n.classList.add("selected");
|
n.classList.add("selected");
|
||||||
|
lastSelected = n;
|
||||||
} else {
|
} else {
|
||||||
n.classList.remove("selected");
|
n.classList.remove("selected");
|
||||||
}
|
}
|
||||||
@@ -597,6 +584,7 @@
|
|||||||
document.getElementById('playlist-edit-trackmove-up').addEventListener("click", (e) => moveSelectedTracks(true));
|
document.getElementById('playlist-edit-trackmove-up').addEventListener("click", (e) => moveSelectedTracks(true));
|
||||||
document.getElementById('playlist-edit-trackmove-down').addEventListener("click", (e) => moveSelectedTracks(false));
|
document.getElementById('playlist-edit-trackmove-down').addEventListener("click", (e) => moveSelectedTracks(false));
|
||||||
document.getElementById('playlist-edit-removetrack').addEventListener("click", (e) => deleteSelectedTracks());
|
document.getElementById('playlist-edit-removetrack').addEventListener("click", (e) => deleteSelectedTracks());
|
||||||
|
document.getElementById('playlist-edit-back').addEventListener("click", (e) => showScreen('playlist'));
|
||||||
document.getElementById('playlist-edit-addtrack').addEventListener("click", (e) => {
|
document.getElementById('playlist-edit-addtrack').addEventListener("click", (e) => {
|
||||||
showScreen("playlist_filebrowser");
|
showScreen("playlist_filebrowser");
|
||||||
});
|
});
|
||||||
@@ -698,6 +686,7 @@
|
|||||||
alert("Failed to save playlist: " + await saveRes.text());
|
alert("Failed to save playlist: " + await saveRes.text());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showScreen('playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveSelectedTracks(up) {
|
function moveSelectedTracks(up) {
|
||||||
@@ -733,6 +722,15 @@
|
|||||||
document.getElementById('playlist-filebrowser-addtrack').addEventListener("click", (e) => {
|
document.getElementById('playlist-filebrowser-addtrack').addEventListener("click", (e) => {
|
||||||
returnSelectedTracks();
|
returnSelectedTracks();
|
||||||
});
|
});
|
||||||
|
document.getElementById('playlist-filebrowser-upload').addEventListener("click", (e) => {
|
||||||
|
uploadFiles();
|
||||||
|
});
|
||||||
|
document.getElementById('playlist-filebrowser-mkdir').addEventListener("click", (e) => {
|
||||||
|
createDirectory();
|
||||||
|
});
|
||||||
|
document.getElementById('playlist-filebrowser-delete').addEventListener("click", (e) => {
|
||||||
|
deleteItems();
|
||||||
|
});
|
||||||
tree.init();
|
tree.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,17 +784,16 @@
|
|||||||
const rootnode = document.createElement('ul');
|
const rootnode = document.createElement('ul');
|
||||||
tree.appendChild(rootnode);
|
tree.appendChild(rootnode);
|
||||||
|
|
||||||
files.sort();
|
files.sort((a, b) => a.name < b.name ? -1 : (a.name > b.name ? 1 : 0));
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const path = file.split('/').slice(1);
|
const path = file.name.split('/').slice(1);
|
||||||
let treeIterator = rootnode;
|
let treeIterator = rootnode;
|
||||||
let curPath = ''
|
let curPath = ''
|
||||||
for (const elem of path) {
|
for (const elem of path) {
|
||||||
curPath += '/' + elem;
|
curPath += '/' + elem;
|
||||||
let node = findChildByName(treeIterator, elem);
|
let node = findChildByName(treeIterator, elem);
|
||||||
if (node === null) {
|
if (node === null) {
|
||||||
const isFile = path.slice(-1)[0] === elem;
|
node = addTreeNode(treeIterator, elem, file.type, curPath);
|
||||||
node = addTreeNode(treeIterator, elem, isFile ? 'file':'directory', curPath);
|
|
||||||
}
|
}
|
||||||
treeIterator = node;
|
treeIterator = node;
|
||||||
}
|
}
|
||||||
@@ -823,6 +820,94 @@
|
|||||||
showScreen("playlist_edit", {addtracks: tracks});
|
showScreen("playlist_edit", {addtracks: tracks});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadFiles() {
|
||||||
|
const tree = document.getElementById("playlist-filebrowser-tree");
|
||||||
|
const selectedNodes = [...tree.querySelectorAll(".selected")];
|
||||||
|
const files = [...document.getElementById("playlist-filebrowser-upload-files").files];
|
||||||
|
if (selectedNodes.length !== 1 ||
|
||||||
|
selectedNodes[0].getAttribute('data-type') !== "directory") {
|
||||||
|
alert("Please select a single directory for upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploadFileHelper(files.length, 0, selectedNodes[0].getAttribute('data-path'), files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFileHelper(totalcount, donecount, destdir, files) {
|
||||||
|
// Upload files sequentially by recursivly calling this function from the 'load' callback when
|
||||||
|
// the previous upload is completed.
|
||||||
|
if (files.length === 0)
|
||||||
|
return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
const ctrl = document.getElementById("playlist-filebrowser-upload-progress");
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const location = destdir + '/' + files[0].name;
|
||||||
|
|
||||||
|
xhr.upload.addEventListener("progress", (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentage = Math.round((e.loaded * 100) / e.total / totalcount + donecount * 100 / totalcount);
|
||||||
|
ctrl.value = percentage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//xhr.upload.addEventListener("load", (e) => {
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (xhr.status !== 204) {
|
||||||
|
alert(`File upload failed: ${xhr.responseText} (${xhr.status})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (donecount + 1 === totalcount) {
|
||||||
|
// Reload file list from device
|
||||||
|
onShow();
|
||||||
|
} else {
|
||||||
|
uploadFileHelper(totalcount, donecount + 1, destdir, files.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.open("POST", `/api/v1/audiofiles?type=file&location=${location}`);
|
||||||
|
xhr.overrideMimeType("audio/mpeg");
|
||||||
|
xhr.send(files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDirectory() {
|
||||||
|
const name = document.getElementById('playlist-filebrowser-mkdir-name');
|
||||||
|
const selectedNodes = [...tree.querySelectorAll(".selected")];
|
||||||
|
const files = [...document.getElementById("playlist-filebrowser-upload-files").files];
|
||||||
|
if (selectedNodes.length > 1 ||
|
||||||
|
(selectedNodes.length === 1 &&
|
||||||
|
selectedNodes[0].getAttribute('data-type') !== "directory")) {
|
||||||
|
alert("Please select a single directory for upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const location = selectedNodes.length === 1
|
||||||
|
? selectedNodes[0].getAttribute('data-path') + '/' + name.value
|
||||||
|
: '/' + name.value;
|
||||||
|
const saveRes = await fetch(`/api/v1/audiofiles?type=directory&location=${location}`,
|
||||||
|
{method: 'POST'});
|
||||||
|
// Reload file list from device
|
||||||
|
onShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteItems() {
|
||||||
|
const tree = document.getElementById("playlist-filebrowser-tree");
|
||||||
|
const selectedNodes = [...tree.querySelectorAll(".selected")];
|
||||||
|
if (selectedNodes.length === 0) {
|
||||||
|
alert("Please select something to delete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = selectedNodes.map(n => n.getAttribute('data-path'));
|
||||||
|
items.sort();
|
||||||
|
items.reverse();
|
||||||
|
for (const item of items) {
|
||||||
|
const saveRes = await fetch(`/api/v1/audiofiles?location=${item}`,
|
||||||
|
{method: 'DELETE'});
|
||||||
|
if (!saveRes.ok) {
|
||||||
|
alert(`Failed to delete item ${item}: ${await saveRes.text()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reload file list from device
|
||||||
|
onShow();
|
||||||
|
}
|
||||||
|
|
||||||
let tree = (() => {
|
let tree = (() => {
|
||||||
let tree = null;
|
let tree = null;
|
||||||
function init() {
|
function init() {
|
||||||
@@ -882,6 +967,14 @@
|
|||||||
return { init, onShow };
|
return { init, onShow };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
async function requestReboot() {
|
||||||
|
const resp = await fetch('/api/v1/reboot/bootloader', {'method': 'POST'});
|
||||||
|
if (!resp.ok) {
|
||||||
|
alert('Reboot to bootloader failed: ' + await resp.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
Object.values(Screens).forEach(screen => {
|
Object.values(Screens).forEach(screen => {
|
||||||
screen.init?.();
|
screen.init?.();
|
||||||
|
|||||||
@@ -161,7 +161,11 @@ class PlayerApp:
|
|||||||
self._onIdle()
|
self._onIdle()
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
print(f'Playing {filename!r}')
|
print(f'Playing {filename!r}')
|
||||||
|
try:
|
||||||
self.mp3file = open(filename, 'rb')
|
self.mp3file = open(filename, 'rb')
|
||||||
|
except OSError as ex:
|
||||||
|
print(f"Could not play file {filename}: {ex}")
|
||||||
|
return
|
||||||
self.player.play(self.mp3file, offset)
|
self.player.play(self.mp3file, offset)
|
||||||
self.paused = False
|
self.paused = False
|
||||||
self._onActive()
|
self._onActive()
|
||||||
@@ -191,3 +195,6 @@ class PlayerApp:
|
|||||||
|
|
||||||
def get_playlist_db(self):
|
def get_playlist_db(self):
|
||||||
return self.playlist_db
|
return self.playlist_db
|
||||||
|
|
||||||
|
def get_leds(self):
|
||||||
|
return self.leds
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import time
|
|||||||
class LedManager:
|
class LedManager:
|
||||||
IDLE = const(0)
|
IDLE = const(0)
|
||||||
PLAYING = const(1)
|
PLAYING = const(1)
|
||||||
|
REBOOTING = const(2)
|
||||||
|
|
||||||
def __init__(self, np):
|
def __init__(self, np):
|
||||||
self.led_state = LedManager.IDLE
|
self.led_state = LedManager.IDLE
|
||||||
@@ -19,7 +20,7 @@ class LedManager:
|
|||||||
asyncio.create_task(self.run())
|
asyncio.create_task(self.run())
|
||||||
|
|
||||||
def set_state(self, state):
|
def set_state(self, state):
|
||||||
assert state in [LedManager.IDLE, LedManager.PLAYING]
|
assert state in [LedManager.IDLE, LedManager.PLAYING, LedManager.REBOOTING]
|
||||||
self.led_state = state
|
self.led_state = state
|
||||||
|
|
||||||
def _gamma(self, value, X=2.2):
|
def _gamma(self, value, X=2.2):
|
||||||
@@ -50,6 +51,8 @@ class LedManager:
|
|||||||
self._pulse(time_, (0, 1, 0), 3)
|
self._pulse(time_, (0, 1, 0), 3)
|
||||||
elif self.led_state == LedManager.PLAYING:
|
elif self.led_state == LedManager.PLAYING:
|
||||||
self._rainbow(time_)
|
self._rainbow(time_)
|
||||||
|
elif self.led_state == LedManager.REBOOTING:
|
||||||
|
self._pulse(time_, (1, 0, 1), 0.2)
|
||||||
time_ += 0.02
|
time_ += 0.02
|
||||||
before = time.ticks_ms()
|
before = time.ticks_ms()
|
||||||
await self.np.async_write()
|
await self.np.async_write()
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hwconfig
|
||||||
import json
|
import json
|
||||||
|
import machine
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
from microdot import Microdot, redirect, send_file
|
from array import array
|
||||||
|
from microdot import Microdot, redirect, send_file, Request
|
||||||
|
from utils import TimerManager, LedManager
|
||||||
|
|
||||||
webapp = Microdot()
|
webapp = Microdot()
|
||||||
server = None
|
server = None
|
||||||
@@ -15,20 +20,26 @@ config = None
|
|||||||
app = None
|
app = None
|
||||||
nfc = None
|
nfc = None
|
||||||
playlist_db = None
|
playlist_db = None
|
||||||
|
leds = None
|
||||||
|
timer_manager = None
|
||||||
|
|
||||||
|
Request.max_content_length = 128 * 1024 * 1024 # 128MB requests allowed
|
||||||
|
|
||||||
|
|
||||||
def start_webserver(config_, app_):
|
def start_webserver(config_, app_):
|
||||||
global server, config, app, nfc, playlist_db
|
global server, config, app, nfc, playlist_db, leds, timer_manager
|
||||||
server = asyncio.create_task(webapp.start_server(port=80))
|
server = asyncio.create_task(webapp.start_server(port=80))
|
||||||
config = config_
|
config = config_
|
||||||
app = app_
|
app = app_
|
||||||
nfc = app.get_nfc()
|
nfc = app.get_nfc()
|
||||||
playlist_db = app.get_playlist_db()
|
playlist_db = app.get_playlist_db()
|
||||||
|
leds = app.get_leds()
|
||||||
|
timer_manager = TimerManager()
|
||||||
|
|
||||||
|
|
||||||
@webapp.before_request
|
@webapp.before_request
|
||||||
async def before_request_handler(request):
|
async def before_request_handler(request):
|
||||||
if request.method in ['PUT', 'POST'] and app.is_playing():
|
if request.method in ['PUT', 'POST', 'DELETE'] and app.is_playing():
|
||||||
return "Cannot write to device while playback is active", 503
|
return "Cannot write to device while playback is active", 503
|
||||||
app.reset_idle_timeout()
|
app.reset_idle_timeout()
|
||||||
|
|
||||||
@@ -159,6 +170,15 @@ async def audiofiles_get(request):
|
|||||||
def directory_iterator():
|
def directory_iterator():
|
||||||
yield '['
|
yield '['
|
||||||
first = True
|
first = True
|
||||||
|
|
||||||
|
def make_json_str(obj):
|
||||||
|
nonlocal first
|
||||||
|
jsonpath = json.dumps(obj)
|
||||||
|
if not first:
|
||||||
|
jsonpath = ',' + jsonpath
|
||||||
|
first = False
|
||||||
|
return jsonpath
|
||||||
|
|
||||||
dirstack = [fsroot]
|
dirstack = [fsroot]
|
||||||
while dirstack:
|
while dirstack:
|
||||||
current_dir = dirstack.pop()
|
current_dir = dirstack.pop()
|
||||||
@@ -167,15 +187,86 @@ async def audiofiles_get(request):
|
|||||||
type_ = entry[1]
|
type_ = entry[1]
|
||||||
current_path = current_dir + b'/' + name
|
current_path = current_dir + b'/' + name
|
||||||
if type_ == 0x4000:
|
if type_ == 0x4000:
|
||||||
|
yield make_json_str({'name': current_path[len(fsroot):], 'type': 'directory'})
|
||||||
dirstack.append(current_path)
|
dirstack.append(current_path)
|
||||||
elif type_ == 0x8000:
|
elif type_ == 0x8000:
|
||||||
if name.lower().endswith('.mp3'):
|
if name.lower().endswith('.mp3'):
|
||||||
jsonpath = json.dumps(current_path[len(fsroot):])
|
yield make_json_str({'name': current_path[len(fsroot):], 'type': 'file'})
|
||||||
if not first:
|
|
||||||
yield ','+jsonpath
|
|
||||||
else:
|
|
||||||
yield jsonpath
|
|
||||||
first = False
|
|
||||||
yield ']'
|
yield ']'
|
||||||
|
|
||||||
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
|
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/audiofiles', methods=['POST'])
|
||||||
|
async def audiofile_upload(request):
|
||||||
|
if 'type' not in request.args or request.args['type'] not in ['file', 'directory']:
|
||||||
|
return 'invalid or missing type', 400
|
||||||
|
if 'location' not in request.args:
|
||||||
|
return 'missing location', 400
|
||||||
|
path = fsroot + '/' + request.args['location']
|
||||||
|
type_ = request.args['type']
|
||||||
|
length = request.content_length
|
||||||
|
print(f'Got upload request of type {type_} to {path} with length {length}')
|
||||||
|
if type_ == 'directory':
|
||||||
|
if length != 0:
|
||||||
|
return 'directory request may not have content', 400
|
||||||
|
os.mkdir(path)
|
||||||
|
return '', 204
|
||||||
|
with open(path, 'wb') as newfile:
|
||||||
|
data = array('b', range(4096))
|
||||||
|
bytes_copied = 0
|
||||||
|
while True:
|
||||||
|
bytes_read = await request.stream.readinto(data)
|
||||||
|
if bytes_read == 0:
|
||||||
|
# End of body
|
||||||
|
break
|
||||||
|
bytes_written = newfile.write(data[:bytes_read])
|
||||||
|
if bytes_written != bytes_read:
|
||||||
|
# short writes shouldn't happen
|
||||||
|
return 'write failure', 500
|
||||||
|
bytes_copied += bytes_written
|
||||||
|
if bytes_copied == length:
|
||||||
|
break
|
||||||
|
if bytes_copied == length:
|
||||||
|
return '', 204
|
||||||
|
else:
|
||||||
|
return 'size mismatch', 500
|
||||||
|
|
||||||
|
|
||||||
|
def recursive_delete(path):
|
||||||
|
stat = os.stat(path)
|
||||||
|
if stat[0] == 0x8000:
|
||||||
|
os.remove(path)
|
||||||
|
elif stat[0] == 0x4000:
|
||||||
|
for entry in os.ilistdir(path):
|
||||||
|
entry_path = path + '/' + entry[0]
|
||||||
|
recursive_delete(entry_path)
|
||||||
|
os.rmdir(path)
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/audiofiles', methods=['DELETE'])
|
||||||
|
async def audiofile_delete(request):
|
||||||
|
if 'location' not in request.args:
|
||||||
|
return 'missing location', 400
|
||||||
|
location = request.args['location']
|
||||||
|
if '..' in location or len(location) == 0:
|
||||||
|
return 'bad location', 400
|
||||||
|
path = fsroot + '/' + request.args['location']
|
||||||
|
recursive_delete(path)
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/reboot/<method>', methods=['POST'])
|
||||||
|
async def reboot(request, method):
|
||||||
|
if hwconfig.get_on_battery():
|
||||||
|
return 'not allowed: usb not connected', 403
|
||||||
|
|
||||||
|
if method == 'bootloader':
|
||||||
|
leds.set_state(LedManager.REBOOTING)
|
||||||
|
timer_manager.schedule(time.ticks_ms() + 1500, machine.bootloader)
|
||||||
|
elif method == 'application':
|
||||||
|
leds.set_state(LedManager.REBOOTING)
|
||||||
|
timer_manager.schedule(time.ticks_ms() + 1500, machine.reset)
|
||||||
|
else:
|
||||||
|
return 'method not supported', 400
|
||||||
|
return '', 204
|
||||||
|
|||||||
Reference in New Issue
Block a user