From 2ec0623073c577841a735931fc1056a391413b1a Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 3 Nov 2026 11:35:32 -0800 Subject: [PATCH] filer UI, admin: upload a folder (#12371) * admin: place uploaded files by their path inside an uploaded folder The multipart file name keeps only the base name, so the upload API takes an optional relativePaths field listing each file's path under the target directory. Paths that are empty, absolute or contain . or .. components are rejected per file. Co-Authored-By: Claude Opus 5.5 * admin: upload a folder from the file browser The upload modal gets a folder picker, and a folder dropped on the listing is walked into, so its files upload with their subfolder paths. Co-Authored-By: Claude Opus 5.5 * filer UI: upload a folder An Upload Folder button picks a whole folder, and a folder dropped on the listing is walked into. Each file is posted to its subfolder under the current directory, which the filer creates on the first upload into it. Progress rows are keyed by that path, so equal names in different subfolders no longer share a row. Co-Authored-By: Claude Opus 5.5 * admin: reject an empty relativePaths field instead of ignoring it Co-Authored-By: Claude Opus 5.5 * admin: upload a large selection in batches below the multipart part limit Go's multipart parser rejects a form of more than 1000 parts, so a folder of about a thousand files failed as a whole. The upload modal now sends 500 files per request and sums the results; progress covers all batches, and the button stays disabled until the last one finishes. Co-Authored-By: Claude Opus 5.5 * admin: escape file names in upload error messages A failed upload reports the client-supplied file or folder path, and the toast renders its message as HTML. Co-Authored-By: Claude Opus 5.5 * admin: list failed uploads in the modal and reload the listing when it closes A partial upload logged its failures only to the console before the page reloaded, and a batch that failed after earlier ones succeeded left the listing stale. The failures now stay in the open modal, and closing it reloads the listing once any file was uploaded. Co-Authored-By: Claude Opus 5.5 * admin: cancel the upload when its modal closes Closing the modal, including with its Cancel button, left the batch loop running, so the remaining batches were still written. The batch in flight is now aborted, which also stops the loop, and the listing reloads since the aborted request may already have stored files. Co-Authored-By: Claude Opus 5.5 * admin: count upload progress by files when the selection has no bytes Co-Authored-By: Claude Opus 5.5 * admin: keep the newest upload selection when a folder drop finishes late Co-Authored-By: Claude Opus 5.5 * admin: cap the upload preview at a fixed number of rows A folder of many thousands of files rendered one preview row per file. Co-Authored-By: Claude Opus 5.5 * filer UI: upload a few files at a time Every selected file started its own request at once, so a large folder opened thousands of requests together. Co-Authored-By: Claude Opus 5.5 * admin: regenerate the gzipped admin.js Co-Authored-By: Claude Opus 5.5 * admin: reload the listing after a batch fails without a result A batch that fails mid-request may still have stored some of its files, so closing the modal reloads the listing. Co-Authored-By: Claude Opus 5.5 * admin: stop reading a folder drop once a newer selection replaces it Co-Authored-By: Claude Opus 5.5 * admin: skip the read error of a folder drop a newer selection replaced Co-Authored-By: Claude Opus 5.5 * filer UI: share the four-upload limit across selections Co-Authored-By: Claude Opus 5.5 * admin: regenerate the gzipped admin.js Co-Authored-By: Claude Opus 5.5 * admin: set up the file browser handlers once admin.js already runs setupFileManagerEventHandlers on DOMContentLoaded, so the file browser page registered every listener twice and each drop was read twice. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- weed/admin/handlers/file_browser_handlers.go | 30 +- .../handlers/file_browser_handlers_test.go | 55 +++ weed/admin/static/js/admin.js | 351 +++++++++++------- weed/admin/static_gz/js/admin.js.gz | Bin 15540 -> 16843 bytes weed/admin/view/app/file_browser.templ | 14 +- weed/admin/view/app/file_browser_templ.go | 8 +- weed/server/filer_ui/filer.html | 111 +++++- 7 files changed, 414 insertions(+), 155 deletions(-) diff --git a/weed/admin/handlers/file_browser_handlers.go b/weed/admin/handlers/file_browser_handlers.go index fb5f122b72..dbe49b9f9c 100644 --- a/weed/admin/handlers/file_browser_handlers.go +++ b/weed/admin/handlers/file_browser_handlers.go @@ -4,6 +4,7 @@ import ( "bufio" "compress/gzip" "context" + "encoding/json" "errors" "fmt" "io" @@ -289,13 +290,26 @@ func (h *FileBrowserHandlers) UploadFile(w http.ResponseWriter, r *http.Request) } } + // A folder upload names each file by its path inside the folder; the multipart + // file name keeps only the base name. + var relativePaths []string + if v, ok := r.MultipartForm.Value["relativePaths"]; ok { + if len(v) != 1 || json.Unmarshal([]byte(v[0]), &relativePaths) != nil || len(relativePaths) != len(files) { + writeJSONError(w, http.StatusBadRequest, "relativePaths must list one path per uploaded file") + return + } + } + var uploadResults []map[string]interface{} var failedUploads []string // Process each uploaded file - for _, fileHeader := range files { + for i, fileHeader := range files { // Validate file name fileName := fileHeader.Filename + if relativePaths != nil { + fileName = relativePaths[i] + } if fileName == "" { failedUploads = append(failedUploads, "invalid filename") continue @@ -303,6 +317,10 @@ func (h *FileBrowserHandlers) UploadFile(w http.ResponseWriter, r *http.Request) // Normalize Windows-style backslashes to forward slashes fileName = util.CleanWindowsPath(fileName) + if relativePaths != nil && !isRelativeUploadPath(fileName) { + failedUploads = append(failedUploads, fmt.Sprintf("%s: invalid relative path", fileName)) + continue + } // Create full path for the file using path.Join for URL path semantics // path.Join handles double slashes and is not OS-specific like filepath.Join @@ -349,6 +367,16 @@ func (h *FileBrowserHandlers) UploadFile(w http.ResponseWriter, r *http.Request) } } +// isRelativeUploadPath reports whether name stays inside the upload directory. +func isRelativeUploadPath(name string) bool { + for _, part := range strings.Split(name, "/") { + if part == "" || part == "." || part == ".." { + return false + } + } + return true +} + // uploadFormOverhead allows for the multipart headers and the path field on // top of the file bytes an upload may carry. Go's multipart parser accepts at // most 1000 parts, whose headers take about half of this. diff --git a/weed/admin/handlers/file_browser_handlers_test.go b/weed/admin/handlers/file_browser_handlers_test.go index 179be2a6a0..1566ebacb3 100644 --- a/weed/admin/handlers/file_browser_handlers_test.go +++ b/weed/admin/handlers/file_browser_handlers_test.go @@ -216,3 +216,58 @@ func TestUploadFile_RemovesSpooledParts(t *testing.T) { t.Errorf("spooled parts left behind: %v", left) } } + +func TestIsRelativeUploadPath(t *testing.T) { + for name, want := range map[string]bool{ + "a.txt": true, + "photos/a.jpg": true, + "a/b/c/..x.txt": true, + "/etc/a": false, + "a/../../b": false, + "..": false, + "a//b": false, + "./a": false, + "a/": false, + } { + if got := isRelativeUploadPath(name); got != want { + t.Errorf("isRelativeUploadPath(%q) = %v, want %v", name, got, want) + } + } +} + +func TestUploadFile_FolderRelativePaths(t *testing.T) { + h := &FileBrowserHandlers{adminServer: &dash.AdminServer{}} + upload := func(relativePaths string) *httptest.ResponseRecorder { + var body bytes.Buffer + mw := multipart.NewWriter(&body) + for _, name := range []string{"a.txt", "b.txt"} { + part, err := mw.CreateFormFile("files", name) + if err != nil { + t.Fatal(err) + } + part.Write([]byte(name)) + } + mw.WriteField("path", "/buckets/b") + mw.WriteField("relativePaths", relativePaths) + mw.Close() + req := httptest.NewRequest(http.MethodPost, "/api/files/upload", &body) + req.Header.Set("Content-Type", mw.FormDataContentType()) + w := httptest.NewRecorder() + h.UploadFile(w, req) + return w + } + + for _, paths := range []string{`["dir/a.txt"]`, `not json`, ``, `null`} { + if w := upload(paths); w.Code != http.StatusBadRequest { + t.Errorf("relativePaths %s: expected 400, got %d (body: %s)", paths, w.Code, w.Body.String()) + } + } + + w := upload(`["../a.txt", "dir\\..\\..\\b.txt"]`) + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected every escaping path to fail, got %d (body: %s)", w.Code, w.Body.String()) + } + if strings.Count(w.Body.String(), "invalid relative path") != 2 { + t.Errorf("expected both paths rejected before upload, got %s", w.Body.String()) + } +} diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index 77681f26fc..ebabb25a32 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -1167,18 +1167,132 @@ async function submitCreateFolder() { } } +// Files dropped on the listing, each with its path under the current directory; +// null when the modal's inputs hold the selection. +let droppedUploadFiles = null; +// Bumped by every drop and picker change, so a slow folder read cannot replace a newer selection. +let uploadSelection = 0; + +function selectedUploadFiles() { + if (droppedUploadFiles) { + return droppedUploadFiles; + } + const selected = []; + ['fileInput', 'folderInput'].forEach(id => { + const input = document.getElementById(id); + Array.from((input && input.files) || []).forEach(file => { + selected.push({ file, path: file.webkitRelativePath || file.name }); + }); + }); + return selected; +} + +// Lists the dropped files, walking into dropped folders and stopping once +// isCurrent turns false. The entries must be taken before the first await, +// while the drop event still exposes them. +async function collectDroppedFiles(dataTransfer, isCurrent) { + const entries = Array.from(dataTransfer.items || []) + .map(item => item.webkitGetAsEntry && item.webkitGetAsEntry()) + .filter(Boolean); + if (entries.length === 0) { + return Array.from(dataTransfer.files).map(file => ({ file, path: file.name })); + } + const collected = []; + async function walk(entry) { + if (!isCurrent()) { + return; + } + if (entry.isFile) { + const file = await new Promise((resolve, reject) => entry.file(resolve, reject)); + collected.push({ file, path: entry.fullPath.replace(/^\/+/, '') }); + return; + } + const reader = entry.createReader(); + while (isCurrent()) { + const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject)); + if (batch.length === 0) { + return; + } + for (const child of batch) { + await walk(child); + } + } + } + for (const entry of entries) { + await walk(entry); + } + return collected; +} + +// Go's multipart parser rejects a form of more than 1000 parts, so a large +// selection goes out in several requests. +const uploadBatchSize = 500; +// Longer file lists show this many rows and a count of the rest. +const uploadListLimit = 200; +// Set once files are uploaded while the modal stays open, so closing it reloads the listing. +let uploadChangedListing = false; +// The batch in flight, aborted when the modal closes. +let activeUploadRequest = null; + +function postUploadBatch(batch, currentPath, onProgress) { + const formData = new FormData(); + batch.forEach(item => { + formData.append('files', item.file); + }); + if (batch.some(item => item.path !== item.file.name)) { + formData.append('relativePaths', JSON.stringify(batch.map(item => item.path))); + } + formData.append('path', currentPath); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener('progress', function (e) { + if (e.lengthComputable) { + onProgress(e.loaded / e.total); + } + }); + xhr.addEventListener('load', function () { + let response = null; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + // reported below + } + // Per-file failures come back with counts, even when all of them failed. + if (response && typeof response.uploaded === 'number') { + resolve(response); + return; + } + if (xhr.status === 200) { + reject(new Error('Upload completed but response format was unexpected')); + return; + } + const errorMessage = (response && (response.error || response.message)) || `Server returned status ${xhr.status}`; + reject(new Error(`Failed to upload files: ${errorMessage}`)); + }); + xhr.addEventListener('error', function () { + reject(new Error('Failed to upload files. Please check your connection and try again.')); + }); + xhr.addEventListener('abort', function () { + reject(new Error('File upload was cancelled.')); + }); + xhr.open('POST', basePath('/api/files/upload')); + activeUploadRequest = xhr; + xhr.send(formData); + }); +} + // Submit upload file form async function submitUploadFile() { - const fileInput = document.getElementById('fileInput'); const currentPath = document.getElementById('uploadPath').value; + const selected = selectedUploadFiles(); - if (!fileInput.files || fileInput.files.length === 0) { - showErrorMessage('Please select at least one file to upload'); + if (selected.length === 0) { + showErrorMessage('Please select at least one file or folder to upload'); return; } - const files = Array.from(fileInput.files); - const totalSize = files.reduce((sum, file) => sum + file.size, 0); + const totalSize = selected.reduce((sum, item) => sum + item.file.size, 0); const maxSize = Number(document.getElementById('uploadFileForm').dataset.maxSize) || 0; if (maxSize > 0 && totalSize > maxSize) { @@ -1186,14 +1300,6 @@ async function submitUploadFile() { return; } - // Individual file size validation removed - no limit per file - - const formData = new FormData(); - files.forEach(file => { - formData.append('files', file); - }); - formData.append('path', currentPath); - // Show progress bar and disable button const progressContainer = document.getElementById('uploadProgress'); const progressBar = progressContainer.querySelector('.progress-bar'); @@ -1201,100 +1307,71 @@ async function submitUploadFile() { const submitButton = document.querySelector('#uploadFileModal .btn-primary'); const originalText = submitButton.innerHTML; + const showProgress = percentComplete => { + progressBar.style.width = percentComplete + '%'; + progressBar.setAttribute('aria-valuenow', percentComplete); + progressBar.textContent = percentComplete + '%'; + uploadStatus.textContent = `Uploading ${selected.length} file(s)... ${percentComplete}%`; + }; + progressContainer.style.display = 'block'; - progressBar.style.width = '0%'; - progressBar.setAttribute('aria-valuenow', '0'); - progressBar.textContent = '0%'; - uploadStatus.textContent = `Uploading ${files.length} file(s)...`; + showProgress(0); + uploadStatus.textContent = `Uploading ${selected.length} file(s)...`; submitButton.disabled = true; submitButton.innerHTML = 'Uploading...'; + const result = { uploaded: 0, failed: 0, errors: [] }; + let failure = ''; try { - const xhr = new XMLHttpRequest(); - - // Handle progress - xhr.upload.addEventListener('progress', function (e) { - if (e.lengthComputable) { - const percentComplete = Math.round((e.loaded / e.total) * 100); - progressBar.style.width = percentComplete + '%'; - progressBar.setAttribute('aria-valuenow', percentComplete); - progressBar.textContent = percentComplete + '%'; - uploadStatus.textContent = `Uploading ${files.length} file(s)... ${percentComplete}%`; + let sentSize = 0; + for (let start = 0; start < selected.length; start += uploadBatchSize) { + const batch = selected.slice(start, start + uploadBatchSize); + const batchSize = batch.reduce((sum, item) => sum + item.file.size, 0); + const response = await postUploadBatch(batch, currentPath, fraction => { + const done = totalSize > 0 + ? (sentSize + fraction * batchSize) / totalSize + : (start + fraction * batch.length) / selected.length; + showProgress(Math.round(done * 100)); + }); + sentSize += batchSize; + result.uploaded += response.uploaded; + result.failed += response.failed; + result.errors.push(...(response.errors || [])); + if (response.uploaded > 0) { + uploadChangedListing = true; } - }); - - // Handle completion - xhr.addEventListener('load', function () { - if (xhr.status === 200) { - try { - const response = JSON.parse(xhr.responseText); - - if (response.uploaded > 0) { - if (response.failed === 0) { - showSuccessMessage(`Successfully uploaded ${response.uploaded} file(s)`); - } else { - showSuccessMessage(response.message); - // Show details of failed uploads - if (response.errors && response.errors.length > 0) { - console.warn('Upload errors:', response.errors); - } - } - - // Hide modal and refresh page - const modal = bootstrap.Modal.getInstance(document.getElementById('uploadFileModal')); - modal.hide(); - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - let errorMessage = response.message || 'All file uploads failed'; - if (response.errors && response.errors.length > 0) { - errorMessage += ': ' + response.errors.join(', '); - } - showErrorMessage(errorMessage); - } - } catch (e) { - showErrorMessage('Upload completed but response format was unexpected'); - } - progressContainer.style.display = 'none'; - } else { - let errorMessage = 'Unknown error'; - try { - const error = JSON.parse(xhr.responseText); - errorMessage = error.error || error.message || errorMessage; - } catch (e) { - errorMessage = `Server returned status ${xhr.status}`; - } - showErrorMessage(`Failed to upload files: ${errorMessage}`); - progressContainer.style.display = 'none'; - } - }); - - // Handle errors - xhr.addEventListener('error', function () { - showErrorMessage('Failed to upload files. Please check your connection and try again.'); - progressContainer.style.display = 'none'; - }); - - // Handle abort - xhr.addEventListener('abort', function () { - showErrorMessage('File upload was cancelled.'); - progressContainer.style.display = 'none'; - }); - - // Send request - xhr.open('POST', basePath('/api/files/upload')); - xhr.send(formData); - + } } catch (error) { console.error('Upload error:', error); - showErrorMessage('Failed to upload files. Please try again.'); - progressContainer.style.display = 'none'; - } finally { - // Re-enable the button - submitButton.disabled = false; - submitButton.innerHTML = originalText; + uploadChangedListing = true; + failure = error.message || 'Failed to upload files. Please try again.'; + if (result.uploaded > 0) { + failure += ` (${result.uploaded} file(s) were uploaded before this)`; + } } + activeUploadRequest = null; + + submitButton.disabled = false; + submitButton.innerHTML = originalText; + + if (!failure && result.failed === 0) { + uploadChangedListing = false; + progressContainer.style.display = 'none'; + showSuccessMessage(`Successfully uploaded ${result.uploaded} file(s)`); + bootstrap.Modal.getInstance(document.getElementById('uploadFileModal')).hide(); + setTimeout(() => { + window.location.reload(); + }, 1000); + return; + } + + if (!failure) { + failure = result.uploaded === 0 ? 'All file uploads failed' : `Uploaded ${result.uploaded} file(s), ${result.failed} failed`; + } + showErrorMessage(escapeHtml(failure)); + uploadStatus.innerHTML = `
${escapeHtml(failure)}
` + + result.errors.slice(0, uploadListLimit).map(error => `
${escapeHtml(error)}
`).join('') + + (result.errors.length > uploadListLimit ? `
and ${result.errors.length - uploadListLimit} more
` : ''); } // Export file list to CSV @@ -1430,12 +1507,16 @@ function setupFileManagerEventHandlers() { } // Handle file selection change to show preview - const fileInput = document.getElementById('fileInput'); - if (fileInput) { - fileInput.addEventListener('change', function (e) { - updateFileListPreview(); - }); - } + ['fileInput', 'folderInput'].forEach(id => { + const input = document.getElementById(id); + if (input) { + input.addEventListener('change', function (e) { + uploadSelection++; + droppedUploadFiles = null; + updateFileListPreview(); + }); + } + }); // Setup checkbox event listeners for file selection const checkboxes = document.querySelectorAll('.file-checkbox'); @@ -1460,10 +1541,20 @@ function setupFileManagerEventHandlers() { const uploadFileModal = document.getElementById('uploadFileModal'); if (uploadFileModal) { uploadFileModal.addEventListener('hidden.bs.modal', function () { + const cancelled = activeUploadRequest; + if (cancelled) { + cancelled.abort(); + } + if (cancelled || uploadChangedListing) { + window.location.reload(); + return; + } const fileInput = document.getElementById('fileInput'); const progressContainer = document.getElementById('uploadProgress'); const fileListPreview = document.getElementById('fileListPreview'); fileInput.value = ''; + document.getElementById('folderInput').value = ''; + droppedUploadFiles = null; progressContainer.style.display = 'none'; fileListPreview.style.display = 'none'; }); @@ -1540,43 +1631,42 @@ function setupDragAndDrop() { } } - function handleDrop(e) { - const dt = e.dataTransfer; - const files = dt.files; - - if (files.length > 0) { - // Open upload modal and set files - const fileInput = document.getElementById('fileInput'); - if (fileInput) { - // Create a new FileList-like object - const fileArray = Array.from(files); - - // Set files to input (this is a bit tricky with file inputs) - const dataTransfer = new DataTransfer(); - fileArray.forEach(file => dataTransfer.items.add(file)); - fileInput.files = dataTransfer.files; - - // Update preview and show modal - updateFileListPreview(); - const modal = new bootstrap.Modal(uploadModal); - modal.show(); + async function handleDrop(e) { + const selection = ++uploadSelection; + let dropped; + try { + dropped = await collectDroppedFiles(e.dataTransfer, () => selection === uploadSelection); + } catch (error) { + console.error('Drop error:', error); + if (selection === uploadSelection) { + showErrorMessage('Failed to read the dropped files.'); } + return; } + if (selection !== uploadSelection || dropped.length === 0) { + return; + } + document.getElementById('fileInput').value = ''; + document.getElementById('folderInput').value = ''; + droppedUploadFiles = dropped; + updateFileListPreview(); + const modal = new bootstrap.Modal(uploadModal); + modal.show(); } } // Update file list preview when files are selected function updateFileListPreview() { - const fileInput = document.getElementById('fileInput'); const fileListPreview = document.getElementById('fileListPreview'); const selectedFilesList = document.getElementById('selectedFilesList'); + const selected = selectedUploadFiles(); - if (!fileInput.files || fileInput.files.length === 0) { + if (selected.length === 0) { fileListPreview.style.display = 'none'; return; } - const files = Array.from(fileInput.files); + const files = selected.map(item => item.file); const totalSize = files.reduce((sum, file) => sum + file.size, 0); let html = `
@@ -1584,16 +1674,19 @@ function updateFileListPreview() { Total: ${formatBytes(totalSize)}
`; - files.forEach((file, index) => { + selected.slice(0, uploadListLimit).forEach(({ file, path }, index) => { const fileIcon = getFileIconByName(file.name); html += `
- ${file.name} + ${escapeHtml(path)}
${formatBytes(file.size)}
`; }); + if (selected.length > uploadListLimit) { + html += `
and ${selected.length - uploadListLimit} more
`; + } selectedFilesList.innerHTML = html; fileListPreview.style.display = 'block'; diff --git a/weed/admin/static_gz/js/admin.js.gz b/weed/admin/static_gz/js/admin.js.gz index d6a6220718b75962a8d0838a051246e1eb26a8bb..328f9f92f0eb098f9ff0a3f42d13b7e784a8775e 100644 GIT binary patch literal 16843 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 K5?_OyL;(O^IzmeT literal 15540 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 Cmr9-h diff --git a/weed/admin/view/app/file_browser.templ b/weed/admin/view/app/file_browser.templ index 7dc84c9069..24db7870df 100644 --- a/weed/admin/view/app/file_browser.templ +++ b/weed/admin/view/app/file_browser.templ @@ -313,7 +313,7 @@ templ FileBrowser(data dash.FileBrowserData) {
- +
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking. if data.UploadMaxSize > 0 { @@ -321,6 +321,13 @@ templ FileBrowser(data dash.FileBrowserData) { }
+
+ + +
+ Uploads every file in the folder, keeping its subfolders. Empty subfolders are skipped. +
+
@@ -401,11 +408,6 @@ templ FileBrowser(data dash.FileBrowserData) { break; } }); - - // Initialize file manager event handlers from admin.js - if (typeof setupFileManagerEventHandlers === 'function') { - setupFileManagerEventHandlers(); - } }); // File browser specific functions diff --git a/weed/admin/view/app/file_browser_templ.go b/weed/admin/view/app/file_browser_templ.go index f994bdd27a..f2a556bb15 100644 --- a/weed/admin/view/app/file_browser_templ.go +++ b/weed/admin/view/app/file_browser_templ.go @@ -865,7 +865,7 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\">
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking. ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\">
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking. ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -888,20 +888,20 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "
Uploads every file in the folder, keeping its subfolders. Empty subfolders are skipped.
0%
Preparing upload...
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\">
0%
Preparing upload...
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/server/filer_ui/filer.html b/weed/server/filer_ui/filer.html index 935f7f808a..5653f41dd8 100644 --- a/weed/server/filer_ui/filer.html +++ b/weed/server/filer_ui/filer.html @@ -39,7 +39,7 @@ background: #ddd; } - #fileElem { + #fileElem, #dirElem { display: none; } @@ -94,6 +94,9 @@ +