Files
seaweedfs/weed/server/filer_ui/filer.html
T
Chris LuandClaude Opus 5.5 878979af70 filer: hide write controls in the UI on the read-only port (#12429)
* filer: hide write controls in the UI on the read-only port

The read-only port accepts only GET and HEAD, but its directory page
offered New Folder, Upload, drag-and-drop, rename and delete, each of
which failed with 405. Requests that come through the read-only handler
now render the page without those controls or the upload script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer: test the directory page controls on each port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer: match the rendered row buttons in the port test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:11:52 -08:00

454 lines
15 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>SeaweedFS Filer {{ .Version }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/seaweedfsstatic/bootstrap/3.3.1/css/bootstrap.min.css">
<style>
body {
padding-bottom: 128px;
}
#drop-area {
border: 1px transparent;
margin-top: 5px;
}
#drop-area.highlight {
border-color: purple;
border: 2px dashed #ccc;
}
.button {
display: inline-block;
padding: 2px;
background: #ccc;
cursor: pointer;
border-radius: 2px;
border: 1px solid #ccc;
float: right;
margin-left: 2px;
margin-bottom: 0;
}
label {
font-weight: normal;
}
.button:hover {
background: #ddd;
}
#fileElem, #dirElem {
display: none;
}
td, th {
vertical-align: bottom;
}
.table-hover > tbody > tr:hover > * > div.operations {
display: block;
}
.table > tbody > tr {
height: 39px;
}
div.operations {
display: none;
}
.footer {
position: absolute;
bottom: 0px;
right: 5%;
min-width: 25%;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.add-files {
font-size: 46px;
text-align: center;
border: 1px dashed #999;
padding-bottom: 9px;
margin: 0 2px;
}
</style>
</head>
<body>
<div class="container">
<div class="page-header">
<h1>
<a href="https://github.com/seaweedfs/seaweedfs"><img src="/seaweedfsstatic/seaweed50x50.png"></img></a>
SeaweedFS Filer <small>{{ .Version }}</small>
</h1>
</div>
<div class="row">
<div>
{{ if not .ReadOnly }}
<div class="btn-group btn-group-sm pull-right" role="group" style="margin-top:3px;">
<label class="btn btn-default" onclick="handleCreateDir()">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> New Folder
</label>
<label class="btn btn-default" for="fileElem">
<span class="glyphicon glyphicon-cloud-upload" aria-hidden="true"></span> Upload
</label>
<label class="btn btn-default" for="dirElem">
<span class="glyphicon glyphicon-folder-close" aria-hidden="true"></span> Upload Folder
</label>
</div>
{{ end }}
<ol class="breadcrumb">
{{ range $entry := .Breadcrumbs }}
<li><a href="{{ printpath $entry.Link }}">
{{ $entry.Name }}
</a></li>
{{ end }}
</ol>
</div>
</div>
<div class="row" id="drop-area">
<form class="upload-form">
<input type="file" id="fileElem" multiple onchange="handleFiles(this.files)">
<input type="file" id="dirElem" webkitdirectory multiple onchange="handleFiles(this.files)">
{{ if .EmptyFolder }}
{{ if not .ReadOnly }}
<div class="row add-files">
+
</div>
{{ end }}
{{ else }}
<table width="100%" class="table table-hover">
{{ $path := .Path }}
{{ $showDirDel := .ShowDirectoryDelete }}
{{ range $entry_index, $entry := .Entries }}
<tr>
<td>
{{ if $entry.IsDirectory }}
<span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span>&nbsp;
<a href="{{ printpath $path "/" $entry.Name "/"}}" >
{{ $entry.Name }}
</a>
{{ else }}
<a href="{{ printpath $path "/" $entry.Name }}" >
{{ $entry.Name }}
</a>
{{ end }}
</td>
<td align="right" nowrap>
{{ if not $entry.IsDirectory }}
{{ $entry.Mime }}&nbsp;
{{ end }}
</td>
<td align="right" nowrap>
{{ if not $entry.IsDirectory }}
{{ $entry.Size | humanizeBytes }}&nbsp;
{{ end }}
</td>
<td align="right" nowrap>
{{ $entry.Timestamp.Format "2006-01-02 15:04" }}
</td>
{{ if not $.ReadOnly }}
<td style="width:75px">
<div class="btn-group btn-group-xs pull-right operations" role="group">
<label class="btn" onclick="handleRename('{{ $entry.Name }}', '{{ printpath $path "/" }}')">
<span class="glyphicon glyphicon-edit" aria-hidden="true"></span>
</label>
{{ if and $entry.IsDirectory $showDirDel }}
<label class="btn" onclick="handleDelete('{{ printpath $path "/" $entry.Name "/" }}')">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
</label>
{{ end }}
{{ if not $entry.IsDirectory }}
<label class="btn" onclick="handleDelete('{{ printpath $path "/" $entry.Name }}')">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
</label>
{{ end }}
</div>
</td>
{{ end }}
</tr>
{{ end }}
</table>
{{ end }}
</form>
</div>
{{ if .ShouldDisplayLoadMore }}
<div class="row">
<a href="{{ printpath .Path }}?limit={{ .Limit }}&lastFileName={{ queryEscape .LastFileName }}" >
Load more
</a>
</div>
{{ end }}
<br/>
<br/>
<div id="progress-area" class="footer" style="display: none;">
</div>
</div>
</body>
{{ if not .ReadOnly }}
<script type="text/javascript">
// ************************ Drag and drop ***************** //
let dropArea = document.getElementById("drop-area");
let progressArea = document.getElementById("progress-area");
// Prevent default drag behaviors
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
// Highlight drop area when item is dragged over it
;['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
;['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
// Handle dropped files
dropArea.addEventListener('drop', handleDrop, false);
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function highlight(e) {
dropArea.classList.add('highlight');
}
function unhighlight(e) {
dropArea.classList.remove('highlight');
}
function handleDrop(e) {
collectDroppedFiles(e.dataTransfer).then(uploadItems, function (err) {
alert('Failed to read the dropped files: ' + err);
});
}
// Lists the dropped files, walking into dropped folders. The entries must be
// taken before the first await, while the drop event still exposes them.
async function collectDroppedFiles(dataTransfer) {
var entries = [...(dataTransfer.items || [])]
.map(item => item.webkitGetAsEntry && item.webkitGetAsEntry())
.filter(Boolean);
if (entries.length === 0) {
return [...dataTransfer.files].map(file => ({'file': file, 'path': file.name}));
}
var collected = [];
async function walk(entry) {
if (entry.isFile) {
var file = await new Promise((resolve, reject) => entry.file(resolve, reject));
collected.push({'file': file, 'path': entry.fullPath.replace(/^\/+/, '')});
return;
}
var reader = entry.createReader();
for (;;) {
var batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
if (batch.length === 0) {
return;
}
for (let child of batch) {
await walk(child);
}
}
}
for (let entry of entries) {
await walk(entry);
}
return collected;
}
function reloadPage() {
window.location.reload(true);
}
var uploadList = {};
function handleFiles(files) {
uploadItems([...files].map(file => ({'file': file, 'path': file.webkitRelativePath || file.name})));
}
// A large folder uploads a few files at a time instead of opening a request per file,
// and a selection made meanwhile joins the same queue.
var uploadConcurrency = 4;
var uploadQueue = [];
var activeUploads = 0;
function uploadItems(items) {
if (items.length === 0) {
return;
}
items.forEach(startUpload);
renderProgress();
uploadQueue.push(...items);
uploadNext();
}
function uploadNext() {
while (activeUploads < uploadConcurrency && uploadQueue.length > 0) {
activeUploads++;
uploadFile(uploadQueue.shift(), function () {
activeUploads--;
uploadNext();
});
}
}
function startUpload(item, i) {
uploadList[item.path] = {'name': item.path, 'percent': 0, 'finish': false};
}
function renderProgress() {
var values = Object.values(uploadList);
var html = '<table class="table">\n<tr><th>Uploading</th><\/tr>\n';
for (let i of values) {
var progressBarClass = 'progress-bar-striped active';
if (i.percent >= 100) {
progressBarClass = 'progress-bar-success';
}
html += '<tr>\n<td>\n';
html += '<div class="progress" style="margin-bottom: 2px;">\n';
html += '<div class="progress-bar ' + progressBarClass + '" role="progressbar" aria-valuenow="' + '100" aria-valuemin="0" aria-valuemax="100" style="width:' + i.percent + '%;">';
html += '<span style="margin-right: 10px;">' + escapeHtml(i.name) + '</span>' + i.percent + '%<\/div>';
html += '<\/div>\n<\/td>\n<\/tr>\n';
}
html += '<\/table>\n';
progressArea.innerHTML = html;
if (values.length > 0) {
progressArea.attributes.style.value = '';
}
}
function reportProgress(file, percent) {
var item = uploadList[file]
item.percent = percent;
renderProgress();
}
function finishUpload(file) {
uploadList[file]['finish'] = true;
renderProgress();
var allFinish = true;
for (let i of Object.values(uploadList)) {
if (!i.finish) {
allFinish = false;
break;
}
}
if (allFinish) {
console.log('All Finish');
reloadPage();
}
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// A file from a folder goes to its subfolder under the current directory,
// which the filer creates on the first upload into it.
function uploadUrl(path) {
var dirs = path.split('/').slice(0, -1);
if (dirs.length === 0) {
return window.location.href;
}
var baseUrl = window.location.origin + window.location.pathname;
if (!baseUrl.endsWith('/')) {
baseUrl += '/';
}
return baseUrl + dirs.map(encodeURIComponent).join('/') + '/' + window.location.search;
}
function uploadFile(item, done) {
var file = item.file;
var url = uploadUrl(item.path);
var xhr = new XMLHttpRequest();
var fileName = item.path;
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
finishUpload(fileName);
done();
}
}
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
var percent = Math.ceil((e.loaded / e.total) * 100);
reportProgress(fileName, percent)
}
});
xhr.upload.addEventListener('loadend', function(e) {
finishUpload(fileName);
});
var formData = new FormData();
xhr.open('POST', url, true);
formData.append('file', file);
xhr.send(formData);
}
function handleCreateDir() {
var dirName = prompt('Folder Name:', '');
dirName = dirName.trim();
if (dirName == null || dirName == '') {
return;
}
var baseUrl = window.location.origin + window.location.pathname;
if (!baseUrl.endsWith('/')) {
baseUrl += '/';
}
var url = baseUrl + encodeURIComponent(dirName);
if (!url.endsWith('/')) {
url += '/';
}
url += window.location.search;
var xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', '');
xhr.send();
reloadPage();
}
function handleRename(originName, basePath) {
var newName = prompt('New Name:', originName);
if (newName == null || newName == '') {
return;
}
var url = basePath + encodeURIComponent(newName);
var originPath = basePath + originName;
url += '?mv.from=' + encodeURIComponent(originPath);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, false);
xhr.setRequestHeader('Content-Type', '');
xhr.send();
reloadPage();
}
function handleDelete(path) {
if (!confirm('Are you sure to delete ' + path + '?')) {
return;
}
var url = path;
if (url.endsWith('/')) {
url += '?recursive=true';
}
var xhr = new XMLHttpRequest();
xhr.open('DELETE', url, false);
xhr.send();
reloadPage();
}
</script>
{{ end }}
</html>