Merge pull request #6769 from tannewt/ww_move

Add file and directory renaming
This commit is contained in:
Scott Shawcroft 2022-08-16 22:11:59 -07:00 committed by GitHub
commit 1287d9c22d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 164 additions and 35 deletions

View File

@ -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/`

View File

@ -10,9 +10,9 @@
<body>
<h1><a href="/"><img src="/favicon.ico"/></a>&nbsp;<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>

View File

@ -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");

View File

@ -17,3 +17,7 @@ body {
margin: 0;
font-size: 0.7em;
}
:disabled {
filter: saturate(0%);
}

View File

@ -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");

View File

@ -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) {