feat: File renaming
parent
98ae384b81
commit
d98b087de2
|
@ -13,10 +13,10 @@ A basic node.js file manager.
|
||||||
- [x] File uploads
|
- [x] File uploads
|
||||||
- [ ] Bulk file uploads
|
- [ ] Bulk file uploads
|
||||||
- [ ] Large file uploads (sharded)
|
- [ ] Large file uploads (sharded)
|
||||||
- [ ] File/folder renaming
|
- [x] File/folder renaming
|
||||||
- [x] Bulk file/folder selection
|
- [x] Bulk file/folder selection
|
||||||
- [x] Delete
|
- [x] Delete
|
||||||
- [ ] Recursive directory delete
|
- [x] Recursive directory delete
|
||||||
- [ ] Move
|
- [ ] Move
|
||||||
- [ ] Copy
|
- [ ] Copy
|
||||||
- [x] Download archive
|
- [x] Download archive
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
function htmlEscape(text) {
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.innerText = text;
|
||||||
|
return p.innerHTML;
|
||||||
|
}
|
|
@ -1,14 +1,8 @@
|
||||||
/* jshint esversion: 6 */
|
/* jshint esversion: 6 */
|
||||||
|
|
||||||
function htmlEscape(text) {
|
|
||||||
const p = document.createElement("p");
|
|
||||||
p.innerText = text;
|
|
||||||
return p.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
let $select = $(".multi-select");
|
let $select = $(".multi-select");
|
||||||
|
|
||||||
let setSelected = (files) => {
|
$select.on("change-files", (e, files) => {
|
||||||
$(".multi-files-value").val(JSON.stringify(files.map((f) => f.name)));
|
$(".multi-files-value").val(JSON.stringify(files.map((f) => f.name)));
|
||||||
if (files.length == 0) {
|
if (files.length == 0) {
|
||||||
$(".multi-files").html(
|
$(".multi-files").html(
|
||||||
|
@ -19,18 +13,15 @@ let setSelected = (files) => {
|
||||||
$(".multi-files").html(
|
$(".multi-files").html(
|
||||||
files
|
files
|
||||||
.map((f) => {
|
.map((f) => {
|
||||||
|
const badge = `<span class="badge rounded-pill bg-secondary badge-alignment">
|
||||||
|
${filesize(f.size)}
|
||||||
|
</span>`;
|
||||||
return `
|
return `
|
||||||
<li class="list-group-item d-flex align-items-start justify-content-between">
|
<li class="list-group-item d-flex align-items-start justify-content-between">
|
||||||
<span class="name">${htmlEscape(f.name)}</span>
|
<span class="name">${htmlEscape(f.name)}</span>
|
||||||
${
|
${f.type == "directory" ? `` : badge}
|
||||||
f.type == "directory"
|
</li>
|
||||||
? ``
|
`;
|
||||||
: `<span class="badge rounded-pill bg-secondary badge-alignment">${filesize(
|
|
||||||
f.size
|
|
||||||
)}</span>`
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
})
|
})
|
||||||
.join("")
|
.join("")
|
||||||
);
|
);
|
||||||
|
@ -44,7 +35,7 @@ let setSelected = (files) => {
|
||||||
} else {
|
} else {
|
||||||
$(".multi-files-total").val(filesize(totalSize));
|
$(".multi-files-total").val(filesize(totalSize));
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const updateSelected = () => {
|
const updateSelected = () => {
|
||||||
let $selected = $(".multi-select:checked");
|
let $selected = $(".multi-select:checked");
|
||||||
|
@ -57,7 +48,7 @@ const updateSelected = () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelected(files);
|
$select.trigger("change-files", [files]);
|
||||||
};
|
};
|
||||||
|
|
||||||
$select.on("change", updateSelected);
|
$select.on("change", updateSelected);
|
||||||
|
|
|
@ -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
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 shellable = process.env.SHELL != "false" && process.env.SHELL;
|
||||||
const cmdable = process.env.CMD != "false" && process.env.CMD;
|
const cmdable = process.env.CMD != "false" && process.env.CMD;
|
||||||
if (shellable || cmdable) {
|
if (shellable || cmdable) {
|
||||||
|
|
|
@ -17,12 +17,16 @@
|
||||||
<script src="/@assets/xterm-addon-fit/lib/xterm-addon-fit.js" defer></script>
|
<script src="/@assets/xterm-addon-fit/lib/xterm-addon-fit.js" defer></script>
|
||||||
<!-- file size math -->
|
<!-- file size math -->
|
||||||
<script src="/@assets/filesize/filesize.js" defer></script>
|
<script src="/@assets/filesize/filesize.js" defer></script>
|
||||||
|
<!-- common utilities -->
|
||||||
|
<script src="/@assets/common.js" defer></script>
|
||||||
<!-- navbar, tooltip -->
|
<!-- navbar, tooltip -->
|
||||||
<link rel="stylesheet" href="/@assets/navbar.css" />
|
<link rel="stylesheet" href="/@assets/navbar.css" />
|
||||||
<script src="/@assets/tooltip.js" defer></script>
|
<script src="/@assets/tooltip.js" defer></script>
|
||||||
<!-- list, multi-select -->
|
<!-- list, multi-select -->
|
||||||
<link rel="stylesheet" href="/@assets/list.css" />
|
<link rel="stylesheet" href="/@assets/list.css" />
|
||||||
<script src="/@assets/multi.js" defer></script>
|
<script src="/@assets/multi.js" defer></script>
|
||||||
|
<!-- rename -->
|
||||||
|
<script src="/@assets/rename.js" defer></script>
|
||||||
<!-- upload -->
|
<!-- upload -->
|
||||||
<link rel="stylesheet" href="/@assets/upload.css" />
|
<link rel="stylesheet" href="/@assets/upload.css" />
|
||||||
<script src="/@assets/upload.js" defer></script>
|
<script src="/@assets/upload.js" defer></script>
|
||||||
|
|
|
@ -57,4 +57,5 @@
|
||||||
{{> dialogue-cmd}}
|
{{> dialogue-cmd}}
|
||||||
|
|
||||||
{{> dialogue-download}}
|
{{> dialogue-download}}
|
||||||
|
{{> dialogue-rename}}
|
||||||
{{> dialogue-delete}}
|
{{> dialogue-delete}}
|
||||||
|
|
|
@ -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>
|
|
@ -30,13 +30,19 @@
|
||||||
<div class="btn-group me-2" role="group">
|
<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">
|
<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"}}
|
{{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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group me-2" role="group">
|
<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">
|
<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"}}
|
{{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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue