Skip to content

Add file and directory renaming #6769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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


Expand Down Expand Up @@ -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.

Expand All @@ -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/`
Expand Down
4 changes: 2 additions & 2 deletions supervisor/shared/web_workflow/static/directory.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
65 changes: 53 additions & 12 deletions supervisor/shared/web_workflow/static/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "") {
Expand Down Expand Up @@ -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 = "📁";
Expand All @@ -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);
}

Expand All @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
4 changes: 4 additions & 0 deletions supervisor/shared/web_workflow/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ body {
margin: 0;
font-size: 0.7em;
}

:disabled {
filter: saturate(0%);
}
2 changes: 1 addition & 1 deletion supervisor/shared/web_workflow/static/welcome.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
83 changes: 65 additions & 18 deletions supervisor/shared/web_workflow/web_workflow.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -986,6 +996,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);
Expand All @@ -1006,23 +1038,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();
Expand Down Expand Up @@ -1066,6 +1083,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;
Expand Down Expand Up @@ -1318,6 +1363,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) {
Expand Down