Browse Source

feat: File renaming

main
Ambrose Chua 2 months ago
parent
commit
d98b087de2
  1. 4
      README.md
  2. 5
      assets/common.js
  3. 31
      assets/multi.js
  4. 62
      assets/rename.js
  5. 51
      index.js
  6. 0
      rootowned/hi
  7. 4
      views/layouts/main.handlebars
  8. 1
      views/list.handlebars
  9. 22
      views/partials/dialogue-rename.handlebars
  10. 10
      views/partials/toolbar.handlebars

4
README.md

@ -13,10 +13,10 @@ A basic node.js file manager.
- [x] File uploads
- [ ] Bulk file uploads
- [ ] Large file uploads (sharded)
- [ ] File/folder renaming
- [x] File/folder renaming
- [x] Bulk file/folder selection
- [x] Delete
- [ ] Recursive directory delete
- [x] Recursive directory delete
- [ ] Move
- [ ] Copy
- [x] Download archive

5
assets/common.js

@ -0,0 +1,5 @@
function htmlEscape(text) {
const p = document.createElement("p");
p.innerText = text;
return p.innerHTML;
}

31
assets/multi.js

@ -1,14 +1,8 @@
/* jshint esversion: 6 */
function htmlEscape(text) {
const p = document.createElement("p");
p.innerText = text;
return p.innerHTML;
}
let $select = $(".multi-select");
let setSelected = (files) => {
$select.on("change-files", (e, files) => {
$(".multi-files-value").val(JSON.stringify(files.map((f) => f.name)));
if (files.length == 0) {
$(".multi-files").html(
@ -19,18 +13,15 @@ let setSelected = (files) => {
$(".multi-files").html(
files
.map((f) => {
const badge = `<span class="badge rounded-pill bg-secondary badge-alignment">
${filesize(f.size)}
</span>`;
return `
<li class="list-group-item d-flex align-items-start justify-content-between">
<span class="name">${htmlEscape(f.name)}</span>
${
f.type == "directory"
? ``
: `<span class="badge rounded-pill bg-secondary badge-alignment">${filesize(
f.size
)}</span>`
}
</li>
`;
<li class="list-group-item d-flex align-items-start justify-content-between">
<span class="name">${htmlEscape(f.name)}</span>
${f.type == "directory" ? `` : badge}
</li>
`;
})
.join("")
);
@ -44,7 +35,7 @@ let setSelected = (files) => {
} else {
$(".multi-files-total").val(filesize(totalSize));
}
};
});
const updateSelected = () => {
let $selected = $(".multi-select:checked");
@ -57,7 +48,7 @@ const updateSelected = () => {
});
});
setSelected(files);
$select.trigger("change-files", [files]);
};
$select.on("change", updateSelected);

62
assets/rename.js

@ -0,0 +1,62 @@
/* jshint esversion: 6 */
const updateRenameValue = ($inputs, $value) => {
let files = [];
$inputs.each((i, ele) => {
files.push({
original: $(ele).data("original"),
new: $(ele).val(),
});
});
$value.val(JSON.stringify(files));
};
$select.on("change-files", (e, files) => {
if (files.length == 0) {
$(".rename-files").html(
`<ul class="list-group"><li class="list-group-item text-muted">No files selected</li></ul>`
);
return;
}
$(".rename-files").html(
files
.map((f) => {
return `
<div class="mb-3">
<label class="form-label">
<strike>${htmlEscape(f.name)}</strike>
</label>
<input
type="text"
class="form-control rename-files-input"
data-original="${htmlEscape(f.name)}"
value="${htmlEscape(f.name)}">
</div>
`;
})
.join("")
);
$(".rename-files-input").on("keydown", (e) => {
if (e.keyCode == 13) {
e.preventDefault();
const $next = $(e.target).parent().nextAll().find("input")[0];
if ($next) {
$next.focus();
if ($next.type == "text") {
$next.select();
}
}
}
});
$(".rename-files").each((i, ele) => {
const $value = $(ele).parent().find(".rename-files-value");
const $inputs = $(ele)
.find(".rename-files-input")
.on("focus blur change", (e) => {
updateRenameValue($inputs, $value);
});
updateRenameValue($inputs, $value);
});
});
updateSelected();

51
index.js

@ -453,6 +453,57 @@ app.get("/*@download", (req, res) => {
});
});
app.post("/*@rename", (req, res) => {
res.filename = req.params[0];
let files = JSON.parse(req.body.files);
if (!files || !files.map) {
req.flash("error", "No files selected.");
res.redirect("back");
return;
}
new Promise((resolve, reject) => {
fs.access(relative(res.filename), fs.constants.W_OK, (err) => {
if (err) {
return reject(err);
}
resolve();
});
})
.then(() => {
let promises = files.map((f) => {
return new Promise((resolve, reject) => {
fs.rename(
relative(res.filename, f.original),
relative(res.filename, f.new),
(err) => {
if (err) {
return reject(err);
}
resolve();
}
);
});
});
Promise.all(promises)
.then(() => {
req.flash("success", "Files renamed. ");
res.redirect("back");
})
.catch((err) => {
console.warn(err);
req.flash("error", "Unable to rename some files: " + err);
res.redirect("back");
});
})
.catch((err) => {
console.warn(err);
req.flash("error", err.toString());
res.redirect("back");
});
});
const shellable = process.env.SHELL != "false" && process.env.SHELL;
const cmdable = process.env.CMD != "false" && process.env.CMD;
if (shellable || cmdable) {

0
rootowned/hi

4
views/layouts/main.handlebars

@ -17,12 +17,16 @@
<script src="/@assets/xterm-addon-fit/lib/xterm-addon-fit.js" defer></script>
<!-- file size math -->
<script src="/@assets/filesize/filesize.js" defer></script>
<!-- common utilities -->
<script src="/@assets/common.js" defer></script>
<!-- navbar, tooltip -->
<link rel="stylesheet" href="/@assets/navbar.css" />
<script src="/@assets/tooltip.js" defer></script>
<!-- list, multi-select -->
<link rel="stylesheet" href="/@assets/list.css" />
<script src="/@assets/multi.js" defer></script>
<!-- rename -->
<script src="/@assets/rename.js" defer></script>
<!-- upload -->
<link rel="stylesheet" href="/@assets/upload.css" />
<script src="/@assets/upload.js" defer></script>

1
views/list.handlebars

@ -57,4 +57,5 @@
{{> dialogue-cmd}}
{{> dialogue-download}}
{{> dialogue-rename}}
{{> dialogue-delete}}

22
views/partials/dialogue-rename.handlebars

@ -0,0 +1,22 @@
<form action="@rename" method="post">
<div id="rename" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title">Rename files or folders</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="rename-files">
</div>
<input type="hidden" name="files" value="" class="rename-files-value" />
</div>
<div class="modal-footer bg-light">
<button type="reset" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
</div>
</form>

10
views/partials/toolbar.handlebars

@ -30,13 +30,19 @@
<div class="btn-group me-2" role="group">
<a class="btn btn-success" href="@download" data-bs-toggle="modal" data-bs-target="#download" title="Download file archive" data-bs-placement="top">
{{octicon "file-zip"}}
<span class="d-none d-md-inline align-text-bottom">Download</span>
<span class="d-none d-lg-inline align-text-bottom">Download</span>
</a>
</div>
<div class="btn-group me-2" role="group">
<a class="btn btn-secondary" href="@rename" data-bs-toggle="modal" data-bs-target="#rename" title="Rename selected files" data-bs-placement="top">
{{octicon "pencil"}}
<span class="d-none d-lg-inline align-text-bottom">Rename</span>
</a>
</div>
<div class="btn-group me-2" role="group">
<a class="btn btn-danger" href="@delete" data-bs-toggle="modal" data-bs-target="#delete" title="Delete selected files" data-bs-placement="top">
{{octicon "trash"}}
<span class="d-none d-md-inline align-text-bottom">Delete</span>
<span class="d-none d-lg-inline align-text-bottom">Delete</span>
</a>
</div>
</div>

Loading…
Cancel
Save