Merge pull request #6769 from tannewt/ww_move
Add file and directory renaming
This commit is contained in:
commit
1287d9c22d
|
@ -202,6 +202,24 @@ Example:
|
|||
curl -v -u :passw0rd -X PUT -L --location-trusted http://circuitpython.local/fs/lib/hello/world/
|
||||
```
|
||||
|
||||
##### Move
|
||||
Moves the directory at the given path to ``X-Destination``. Also known as rename.
|
||||
|
||||
The custom `X-Destination` header stores the destination path of the directory.
|
||||
|
||||
* `201 Created` - Directory renamed
|
||||
* `401 Unauthorized` - Incorrect password
|
||||
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
|
||||
* `404 Not Found` - Source directory not found or destination path is missing
|
||||
* `409 Conflict` - USB is active and preventing file system modification
|
||||
* `412 Precondition Failed` - The destination path is already in use
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -v -u :passw0rd -X MOVE -H "X-Destination: /fs/lib/hello2/" -L --location-trusted http://circuitpython.local/fs/lib/hello/
|
||||
```
|
||||
|
||||
##### DELETE
|
||||
Deletes the directory and all of its contents.
|
||||
|
||||
|
@ -214,7 +232,7 @@ Deletes the directory and all of its contents.
|
|||
Example:
|
||||
|
||||
```sh
|
||||
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world/
|
||||
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello2/world/
|
||||
```
|
||||
|
||||
|
||||
|
@ -270,6 +288,25 @@ curl -v -u :passw0rd -L --location-trusted http://circuitpython.local/fs/lib/hel
|
|||
```
|
||||
|
||||
|
||||
##### Move
|
||||
Moves the file at the given path to the ``X-Destination``. Also known as rename.
|
||||
|
||||
The custom `X-Destination` header stores the destination path of the file.
|
||||
|
||||
* `201 Created` - File renamed
|
||||
* `401 Unauthorized` - Incorrect password
|
||||
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
|
||||
* `404 Not Found` - Source file not found or destination path is missing
|
||||
* `409 Conflict` - USB is active and preventing file system modification
|
||||
* `412 Precondition Failed` - The destination path is already in use
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -v -u :passw0rd -X MOVE -H "X-Destination: /fs/lib/hello/world2.txt" -L --location-trusted http://circuitpython.local/fs/lib/hello/world.txt
|
||||
```
|
||||
|
||||
|
||||
##### DELETE
|
||||
Deletes the file.
|
||||
|
||||
|
@ -283,7 +320,7 @@ Deletes the file.
|
|||
Example:
|
||||
|
||||
```sh
|
||||
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world.txt
|
||||
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world2.txt
|
||||
```
|
||||
|
||||
### `/cp/`
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
<body>
|
||||
<h1><a href="/"><img src="/favicon.ico"/></a> <span id="path"></span></h1>
|
||||
<div id="usbwarning" style="display: none;">ℹ️ USB is using the storage. Only allowing reads. See <a href="https://learn.adafruit.com/circuitpython-essentials/circuitpython-storage">the CircuitPython Essentials: Storage guide</a> for details.</div>
|
||||
<template id="row"><tr><td></td><td></td><td><a></a></td><td></td><td><button class="delete">🗑️</button></td><td><a class="edit_link" href="">Edit</a></td></tr></template>
|
||||
<template id="row"><tr><td></td><td></td><td><a class="path"></a></td><td class="modtime"></td><td><button class="rename">✏️ Rename</button></td><td><button class="delete">🗑️ Delete</button></td><td><a class="edit_link" href=""><button>📝 Edit</button></a></td></tr></template>
|
||||
<table>
|
||||
<thead><tr><th>Type</th><th>Size</th><th>Path</th><th>Modified</th><th></th></tr></thead>
|
||||
<thead><tr><th>Type</th><th>Size</th><th>Path</th><th>Modified</th><th colspan="3"></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<hr>
|
||||
|
|
|
@ -5,15 +5,19 @@ var url_base = window.location;
|
|||
var current_path;
|
||||
var editable = undefined;
|
||||
|
||||
function compareValues(a, b) {
|
||||
if (a.directory == b.directory && a.name.toLowerCase() === b.name.toLowerCase()) {
|
||||
return 0;
|
||||
} else if (a.directory != b.directory) {
|
||||
return a.directory < b.directory ? 1 : -1;
|
||||
} else {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh_list() {
|
||||
|
||||
function compareValues(a, b) {
|
||||
if (a.directory == b.directory && a.name.toLowerCase() === b.name.toLowerCase()) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.directory.toString().substring(3,4)+a.name.toLowerCase() < b.directory.toString().substring(3,4)+b.name.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
current_path = window.location.hash.substr(1);
|
||||
if (current_path == "") {
|
||||
|
@ -52,7 +56,7 @@ async function refresh_list() {
|
|||
}
|
||||
}
|
||||
|
||||
if (window.location.path != "/fs/") {
|
||||
if (current_path != "/") {
|
||||
var clone = template.content.cloneNode(true);
|
||||
var td = clone.querySelectorAll("td");
|
||||
td[0].textContent = "📁";
|
||||
|
@ -62,6 +66,8 @@ async function refresh_list() {
|
|||
path.textContent = "..";
|
||||
// Remove the delete button
|
||||
td[4].replaceChildren();
|
||||
td[5].replaceChildren();
|
||||
td[6].replaceChildren();
|
||||
new_children.push(clone);
|
||||
}
|
||||
|
||||
|
@ -82,31 +88,46 @@ async function refresh_list() {
|
|||
file_path = api_url;
|
||||
}
|
||||
|
||||
var text_file = false;
|
||||
if (f.directory) {
|
||||
icon = "📁";
|
||||
} else if(f.name.endsWith(".txt") ||
|
||||
f.name.endsWith(".env") ||
|
||||
f.name.endsWith(".py") ||
|
||||
f.name.endsWith(".js") ||
|
||||
f.name.endsWith(".json")) {
|
||||
icon = "📄";
|
||||
text_file = true;
|
||||
} else if (f.name.endsWith(".html")) {
|
||||
icon = "🌐";
|
||||
text_file = true;
|
||||
}
|
||||
td[0].textContent = icon;
|
||||
td[1].textContent = f.file_size;
|
||||
var path = clone.querySelector("a");
|
||||
var path = clone.querySelector("a.path");
|
||||
path.href = file_path;
|
||||
path.textContent = f.name;
|
||||
td[3].textContent = (new Date(f.modified_ns / 1000000)).toLocaleString();
|
||||
let modtime = clone.querySelector("td.modtime");
|
||||
modtime.textContent = (new Date(f.modified_ns / 1000000)).toLocaleString();
|
||||
var delete_button = clone.querySelector("button.delete");
|
||||
delete_button.value = api_url;
|
||||
delete_button.disabled = !editable;
|
||||
delete_button.onclick = del;
|
||||
|
||||
if (editable && !f.directory) {
|
||||
|
||||
var rename_button = clone.querySelector("button.rename");
|
||||
rename_button.value = api_url;
|
||||
rename_button.disabled = !editable;
|
||||
rename_button.onclick = rename;
|
||||
|
||||
let edit_link = clone.querySelector(".edit_link");
|
||||
if (text_file && editable && !f.directory) {
|
||||
edit_url = new URL(edit_url, url_base);
|
||||
let edit_link = clone.querySelector(".edit_link");
|
||||
edit_link.href = edit_url
|
||||
} else if (f.directory) {
|
||||
edit_link.style = "display: none;";
|
||||
} else {
|
||||
edit_link.querySelector("button").disabled = true;
|
||||
}
|
||||
|
||||
new_children.push(clone);
|
||||
|
@ -188,6 +209,26 @@ async function del(e) {
|
|||
}
|
||||
}
|
||||
|
||||
async function rename(e) {
|
||||
let fn = new URL(e.target.value);
|
||||
var new_fn = prompt("Rename to ", fn.pathname.substr(3));
|
||||
if (new_fn === null) {
|
||||
return;
|
||||
}
|
||||
let new_uri = new URL("/fs" + new_fn, fn);
|
||||
const response = await fetch(e.target.value,
|
||||
{
|
||||
method: "MOVE",
|
||||
headers: {
|
||||
'X-Destination': new_uri.pathname,
|
||||
},
|
||||
}
|
||||
)
|
||||
if (response.ok) {
|
||||
refresh_list();
|
||||
}
|
||||
}
|
||||
|
||||
find_devices();
|
||||
|
||||
let mkdir_button = document.getElementById("mkdir");
|
||||
|
|
|
@ -17,3 +17,7 @@ body {
|
|||
margin: 0;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
filter: saturate(0%);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
var url_base = window.location;
|
||||
var current_path;
|
||||
|
||||
var mdns_works = window.location.hostname.endsWith(".local");
|
||||
var mdns_works = url_base.hostname.endsWith(".local");
|
||||
|
||||
async function find_devices() {
|
||||
var version_response = await fetch("/cp/version.json");
|
||||
|
|
|
@ -78,8 +78,9 @@ typedef struct {
|
|||
enum request_state state;
|
||||
char method[8];
|
||||
char path[256];
|
||||
char destination[256];
|
||||
char header_key[64];
|
||||
char header_value[64];
|
||||
char header_value[256];
|
||||
// We store the origin so we can reply back with it.
|
||||
char origin[64];
|
||||
size_t content_length;
|
||||
|
@ -553,6 +554,15 @@ static void _reply_conflict(socketpool_socket_obj_t *socket, _request *request)
|
|||
_send_str(socket, "\r\nUSB storage active.");
|
||||
}
|
||||
|
||||
|
||||
static void _reply_precondition_failed(socketpool_socket_obj_t *socket, _request *request) {
|
||||
_send_strs(socket,
|
||||
"HTTP/1.1 412 Precondition Failed\r\n",
|
||||
"Content-Length: 0\r\n", NULL);
|
||||
_cors_header(socket, request);
|
||||
_send_str(socket, "\r\n");
|
||||
}
|
||||
|
||||
static void _reply_payload_too_large(socketpool_socket_obj_t *socket, _request *request) {
|
||||
_send_strs(socket,
|
||||
"HTTP/1.1 413 Payload Too Large\r\n",
|
||||
|
@ -987,6 +997,28 @@ static uint8_t _hex2nibble(char h) {
|
|||
return h - 'a' + 0xa;
|
||||
}
|
||||
|
||||
// Decode percent encoding in place. Only do this once on a string!
|
||||
static void _decode_percents(char *str) {
|
||||
size_t o = 0;
|
||||
size_t i = 0;
|
||||
size_t startlen = strlen(str);
|
||||
while (i < startlen) {
|
||||
if (str[i] == '%') {
|
||||
str[o] = _hex2nibble(str[i + 1]) << 4 | _hex2nibble(str[i + 2]);
|
||||
i += 3;
|
||||
} else {
|
||||
if (i != o) {
|
||||
str[o] = str[i];
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
o += 1;
|
||||
}
|
||||
if (o < i) {
|
||||
str[o] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static bool _reply(socketpool_socket_obj_t *socket, _request *request) {
|
||||
if (request->redirect) {
|
||||
_reply_redirect(socket, request, request->path);
|
||||
|
@ -1007,23 +1039,8 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) {
|
|||
// Decode any percent encoded bytes so that we're left with UTF-8.
|
||||
// We only do this on /fs/ paths and after redirect so that any
|
||||
// path echoing we do stays encoded.
|
||||
size_t o = 0;
|
||||
size_t i = 0;
|
||||
while (i < strlen(request->path)) {
|
||||
if (request->path[i] == '%') {
|
||||
request->path[o] = _hex2nibble(request->path[i + 1]) << 4 | _hex2nibble(request->path[i + 2]);
|
||||
i += 3;
|
||||
} else {
|
||||
if (i != o) {
|
||||
request->path[o] = request->path[i];
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
o += 1;
|
||||
}
|
||||
if (o < i) {
|
||||
request->path[o] = '\0';
|
||||
}
|
||||
_decode_percents(request->path);
|
||||
|
||||
char *path = request->path + 3;
|
||||
size_t pathlen = strlen(path);
|
||||
FATFS *fs = filesystem_circuitpy();
|
||||
|
@ -1067,6 +1084,34 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) {
|
|||
_reply_no_content(socket, request);
|
||||
return true;
|
||||
}
|
||||
} else if (strcasecmp(request->method, "MOVE") == 0) {
|
||||
if (_usb_active()) {
|
||||
_reply_conflict(socket, request);
|
||||
return false;
|
||||
}
|
||||
|
||||
_decode_percents(request->destination);
|
||||
char *destination = request->destination + 3;
|
||||
size_t destinationlen = strlen(destination);
|
||||
if (destination[destinationlen - 1] == '/' && destinationlen > 1) {
|
||||
destination[destinationlen - 1] = '\0';
|
||||
}
|
||||
|
||||
FRESULT result = f_rename(fs, path, destination);
|
||||
#if CIRCUITPY_USB_MSC
|
||||
usb_msc_unlock();
|
||||
#endif
|
||||
if (result == FR_EXIST) { // File exists and won't be overwritten.
|
||||
_reply_precondition_failed(socket, request);
|
||||
} else if (result == FR_NO_PATH || result == FR_NO_FILE) { // Missing higher directories or target file.
|
||||
_reply_missing(socket, request);
|
||||
} else if (result != FR_OK) {
|
||||
ESP_LOGE(TAG, "move error %d %s", result, path);
|
||||
_reply_server_error(socket, request);
|
||||
} else {
|
||||
_reply_created(socket, request);
|
||||
return true;
|
||||
}
|
||||
} else if (directory) {
|
||||
if (strcasecmp(request->method, "GET") == 0) {
|
||||
FF_DIR dir;
|
||||
|
@ -1321,6 +1366,8 @@ static void _process_request(socketpool_socket_obj_t *socket, _request *request)
|
|||
} else if (strcasecmp(request->header_key, "Sec-WebSocket-Key") == 0 &&
|
||||
strlen(request->header_value) == 24) {
|
||||
strcpy(request->websocket_key, request->header_value);
|
||||
} else if (strcasecmp(request->header_key, "X-Destination") == 0) {
|
||||
strcpy(request->destination, request->header_value);
|
||||
}
|
||||
ESP_LOGI(TAG, "Header %s %s", request->header_key, request->header_value);
|
||||
} else if (request->offset > sizeof(request->header_value) - 1) {
|
||||
|
|
Loading…
Reference in New Issue