Skip to content

Commit 1287d9c

Browse files
authored
Merge pull request #6769 from tannewt/ww_move
Add file and directory renaming
2 parents e6f1a2b + e570349 commit 1287d9c

File tree

6 files changed

+164
-35
lines changed

6 files changed

+164
-35
lines changed

docs/workflows.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,24 @@ Example:
202202
curl -v -u :passw0rd -X PUT -L --location-trusted http://circuitpython.local/fs/lib/hello/world/
203203
```
204204

205+
##### Move
206+
Moves the directory at the given path to ``X-Destination``. Also known as rename.
207+
208+
The custom `X-Destination` header stores the destination path of the directory.
209+
210+
* `201 Created` - Directory renamed
211+
* `401 Unauthorized` - Incorrect password
212+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
213+
* `404 Not Found` - Source directory not found or destination path is missing
214+
* `409 Conflict` - USB is active and preventing file system modification
215+
* `412 Precondition Failed` - The destination path is already in use
216+
217+
Example:
218+
219+
```sh
220+
curl -v -u :passw0rd -X MOVE -H "X-Destination: /fs/lib/hello2/" -L --location-trusted http://circuitpython.local/fs/lib/hello/
221+
```
222+
205223
##### DELETE
206224
Deletes the directory and all of its contents.
207225

@@ -214,7 +232,7 @@ Deletes the directory and all of its contents.
214232
Example:
215233

216234
```sh
217-
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world/
235+
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello2/world/
218236
```
219237

220238

@@ -270,6 +288,25 @@ curl -v -u :passw0rd -L --location-trusted http://circuitpython.local/fs/lib/hel
270288
```
271289

272290

291+
##### Move
292+
Moves the file at the given path to the ``X-Destination``. Also known as rename.
293+
294+
The custom `X-Destination` header stores the destination path of the file.
295+
296+
* `201 Created` - File renamed
297+
* `401 Unauthorized` - Incorrect password
298+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
299+
* `404 Not Found` - Source file not found or destination path is missing
300+
* `409 Conflict` - USB is active and preventing file system modification
301+
* `412 Precondition Failed` - The destination path is already in use
302+
303+
Example:
304+
305+
```sh
306+
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
307+
```
308+
309+
273310
##### DELETE
274311
Deletes the file.
275312

@@ -283,7 +320,7 @@ Deletes the file.
283320
Example:
284321

285322
```sh
286-
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world.txt
323+
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world2.txt
287324
```
288325

289326
### `/cp/`

supervisor/shared/web_workflow/static/directory.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
<body>
1111
<h1><a href="/"><img src="/favicon.ico"/></a>&nbsp;<span id="path"></span></h1>
1212
<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>
13-
<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>
13+
<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>
1414
<table>
15-
<thead><tr><th>Type</th><th>Size</th><th>Path</th><th>Modified</th><th></th></tr></thead>
15+
<thead><tr><th>Type</th><th>Size</th><th>Path</th><th>Modified</th><th colspan="3"></th></tr></thead>
1616
<tbody></tbody>
1717
</table>
1818
<hr>

supervisor/shared/web_workflow/static/directory.js

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ var url_base = window.location;
55
var current_path;
66
var editable = undefined;
77

8+
function compareValues(a, b) {
9+
if (a.directory == b.directory && a.name.toLowerCase() === b.name.toLowerCase()) {
10+
return 0;
11+
} else if (a.directory != b.directory) {
12+
return a.directory < b.directory ? 1 : -1;
13+
} else {
14+
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
15+
}
16+
}
17+
818
async function refresh_list() {
919

10-
function compareValues(a, b) {
11-
if (a.directory == b.directory && a.name.toLowerCase() === b.name.toLowerCase()) {
12-
return 0;
13-
} else {
14-
return a.directory.toString().substring(3,4)+a.name.toLowerCase() < b.directory.toString().substring(3,4)+b.name.toLowerCase() ? -1 : 1;
15-
}
16-
}
20+
1721

1822
current_path = window.location.hash.substr(1);
1923
if (current_path == "") {
@@ -52,7 +56,7 @@ async function refresh_list() {
5256
}
5357
}
5458

55-
if (window.location.path != "/fs/") {
59+
if (current_path != "/") {
5660
var clone = template.content.cloneNode(true);
5761
var td = clone.querySelectorAll("td");
5862
td[0].textContent = "📁";
@@ -62,6 +66,8 @@ async function refresh_list() {
6266
path.textContent = "..";
6367
// Remove the delete button
6468
td[4].replaceChildren();
69+
td[5].replaceChildren();
70+
td[6].replaceChildren();
6571
new_children.push(clone);
6672
}
6773

@@ -82,31 +88,46 @@ async function refresh_list() {
8288
file_path = api_url;
8389
}
8490

91+
var text_file = false;
8592
if (f.directory) {
8693
icon = "📁";
8794
} else if(f.name.endsWith(".txt") ||
95+
f.name.endsWith(".env") ||
8896
f.name.endsWith(".py") ||
8997
f.name.endsWith(".js") ||
9098
f.name.endsWith(".json")) {
9199
icon = "📄";
100+
text_file = true;
92101
} else if (f.name.endsWith(".html")) {
93102
icon = "🌐";
103+
text_file = true;
94104
}
95105
td[0].textContent = icon;
96106
td[1].textContent = f.file_size;
97-
var path = clone.querySelector("a");
107+
var path = clone.querySelector("a.path");
98108
path.href = file_path;
99109
path.textContent = f.name;
100-
td[3].textContent = (new Date(f.modified_ns / 1000000)).toLocaleString();
110+
let modtime = clone.querySelector("td.modtime");
111+
modtime.textContent = (new Date(f.modified_ns / 1000000)).toLocaleString();
101112
var delete_button = clone.querySelector("button.delete");
102113
delete_button.value = api_url;
103114
delete_button.disabled = !editable;
104115
delete_button.onclick = del;
105116

106-
if (editable && !f.directory) {
117+
118+
var rename_button = clone.querySelector("button.rename");
119+
rename_button.value = api_url;
120+
rename_button.disabled = !editable;
121+
rename_button.onclick = rename;
122+
123+
let edit_link = clone.querySelector(".edit_link");
124+
if (text_file && editable && !f.directory) {
107125
edit_url = new URL(edit_url, url_base);
108-
let edit_link = clone.querySelector(".edit_link");
109126
edit_link.href = edit_url
127+
} else if (f.directory) {
128+
edit_link.style = "display: none;";
129+
} else {
130+
edit_link.querySelector("button").disabled = true;
110131
}
111132

112133
new_children.push(clone);
@@ -188,6 +209,26 @@ async function del(e) {
188209
}
189210
}
190211

212+
async function rename(e) {
213+
let fn = new URL(e.target.value);
214+
var new_fn = prompt("Rename to ", fn.pathname.substr(3));
215+
if (new_fn === null) {
216+
return;
217+
}
218+
let new_uri = new URL("/fs" + new_fn, fn);
219+
const response = await fetch(e.target.value,
220+
{
221+
method: "MOVE",
222+
headers: {
223+
'X-Destination': new_uri.pathname,
224+
},
225+
}
226+
)
227+
if (response.ok) {
228+
refresh_list();
229+
}
230+
}
231+
191232
find_devices();
192233

193234
let mkdir_button = document.getElementById("mkdir");

supervisor/shared/web_workflow/static/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ body {
1717
margin: 0;
1818
font-size: 0.7em;
1919
}
20+
21+
:disabled {
22+
filter: saturate(0%);
23+
}

supervisor/shared/web_workflow/static/welcome.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
var url_base = window.location;
22
var current_path;
33

4-
var mdns_works = window.location.hostname.endsWith(".local");
4+
var mdns_works = url_base.hostname.endsWith(".local");
55

66
async function find_devices() {
77
var version_response = await fetch("/cp/version.json");

supervisor/shared/web_workflow/web_workflow.c

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ typedef struct {
7878
enum request_state state;
7979
char method[8];
8080
char path[256];
81+
char destination[256];
8182
char header_key[64];
82-
char header_value[64];
83+
char header_value[256];
8384
// We store the origin so we can reply back with it.
8485
char origin[64];
8586
size_t content_length;
@@ -553,6 +554,15 @@ static void _reply_conflict(socketpool_socket_obj_t *socket, _request *request)
553554
_send_str(socket, "\r\nUSB storage active.");
554555
}
555556

557+
558+
static void _reply_precondition_failed(socketpool_socket_obj_t *socket, _request *request) {
559+
_send_strs(socket,
560+
"HTTP/1.1 412 Precondition Failed\r\n",
561+
"Content-Length: 0\r\n", NULL);
562+
_cors_header(socket, request);
563+
_send_str(socket, "\r\n");
564+
}
565+
556566
static void _reply_payload_too_large(socketpool_socket_obj_t *socket, _request *request) {
557567
_send_strs(socket,
558568
"HTTP/1.1 413 Payload Too Large\r\n",
@@ -987,6 +997,28 @@ static uint8_t _hex2nibble(char h) {
987997
return h - 'a' + 0xa;
988998
}
989999

1000+
// Decode percent encoding in place. Only do this once on a string!
1001+
static void _decode_percents(char *str) {
1002+
size_t o = 0;
1003+
size_t i = 0;
1004+
size_t startlen = strlen(str);
1005+
while (i < startlen) {
1006+
if (str[i] == '%') {
1007+
str[o] = _hex2nibble(str[i + 1]) << 4 | _hex2nibble(str[i + 2]);
1008+
i += 3;
1009+
} else {
1010+
if (i != o) {
1011+
str[o] = str[i];
1012+
}
1013+
i += 1;
1014+
}
1015+
o += 1;
1016+
}
1017+
if (o < i) {
1018+
str[o] = '\0';
1019+
}
1020+
}
1021+
9901022
static bool _reply(socketpool_socket_obj_t *socket, _request *request) {
9911023
if (request->redirect) {
9921024
_reply_redirect(socket, request, request->path);
@@ -1007,23 +1039,8 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) {
10071039
// Decode any percent encoded bytes so that we're left with UTF-8.
10081040
// We only do this on /fs/ paths and after redirect so that any
10091041
// path echoing we do stays encoded.
1010-
size_t o = 0;
1011-
size_t i = 0;
1012-
while (i < strlen(request->path)) {
1013-
if (request->path[i] == '%') {
1014-
request->path[o] = _hex2nibble(request->path[i + 1]) << 4 | _hex2nibble(request->path[i + 2]);
1015-
i += 3;
1016-
} else {
1017-
if (i != o) {
1018-
request->path[o] = request->path[i];
1019-
}
1020-
i += 1;
1021-
}
1022-
o += 1;
1023-
}
1024-
if (o < i) {
1025-
request->path[o] = '\0';
1026-
}
1042+
_decode_percents(request->path);
1043+
10271044
char *path = request->path + 3;
10281045
size_t pathlen = strlen(path);
10291046
FATFS *fs = filesystem_circuitpy();
@@ -1067,6 +1084,34 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) {
10671084
_reply_no_content(socket, request);
10681085
return true;
10691086
}
1087+
} else if (strcasecmp(request->method, "MOVE") == 0) {
1088+
if (_usb_active()) {
1089+
_reply_conflict(socket, request);
1090+
return false;
1091+
}
1092+
1093+
_decode_percents(request->destination);
1094+
char *destination = request->destination + 3;
1095+
size_t destinationlen = strlen(destination);
1096+
if (destination[destinationlen - 1] == '/' && destinationlen > 1) {
1097+
destination[destinationlen - 1] = '\0';
1098+
}
1099+
1100+
FRESULT result = f_rename(fs, path, destination);
1101+
#if CIRCUITPY_USB_MSC
1102+
usb_msc_unlock();
1103+
#endif
1104+
if (result == FR_EXIST) { // File exists and won't be overwritten.
1105+
_reply_precondition_failed(socket, request);
1106+
} else if (result == FR_NO_PATH || result == FR_NO_FILE) { // Missing higher directories or target file.
1107+
_reply_missing(socket, request);
1108+
} else if (result != FR_OK) {
1109+
ESP_LOGE(TAG, "move error %d %s", result, path);
1110+
_reply_server_error(socket, request);
1111+
} else {
1112+
_reply_created(socket, request);
1113+
return true;
1114+
}
10701115
} else if (directory) {
10711116
if (strcasecmp(request->method, "GET") == 0) {
10721117
FF_DIR dir;
@@ -1321,6 +1366,8 @@ static void _process_request(socketpool_socket_obj_t *socket, _request *request)
13211366
} else if (strcasecmp(request->header_key, "Sec-WebSocket-Key") == 0 &&
13221367
strlen(request->header_value) == 24) {
13231368
strcpy(request->websocket_key, request->header_value);
1369+
} else if (strcasecmp(request->header_key, "X-Destination") == 0) {
1370+
strcpy(request->destination, request->header_value);
13241371
}
13251372
ESP_LOGI(TAG, "Header %s %s", request->header_key, request->header_value);
13261373
} else if (request->offset > sizeof(request->header_value) - 1) {

0 commit comments

Comments
 (0)