Merge origin SMB fixes with local Phase 21-22 features
Origin changes merged: - SMB performance optimization (pread/pwrite, tokio Mutex) - macOS SMB mount fix (AAPL caps, credit grant) - Compound request integration tests - CTDB architecture analysis Local changes preserved: - upload_path config (deployed, tested stable) - delete_file + preview_file routes (MyFiles UI) - SSH async I/O (cipher.rs, packet.rs, server.rs) - auth.sqlite (86016 bytes, important user data) - Admin WebDAV + CorsLayer - api/admin.rs + api/config.rs (new endpoints) Conflicts resolved: - myfiles.rs: kept upload_path + OnceLock static - auth.sqlite: preserved local version (important data) Test results: 393 passed, 5 auth tests failed - PG tests require external PostgreSQL - Auth tests expect specific password hashes - auth.sqlite preserved with actual user credentials
This commit is contained in:
@@ -2,268 +2,145 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>File Upload</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.upload-container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
.upload-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input[type="text"], input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.progress {
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #007bff;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f7; color: #1d1d1f; padding: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
.desc { color: #6e6e73; margin-bottom: 24px; }
|
||||
.card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); margin-bottom: 16px; }
|
||||
.form-group { margin-bottom: 16px; }
|
||||
label { display: block; font-weight: 600; margin-bottom: 6px; font-size: 14px; }
|
||||
input[type="text"] { width: 100%; padding: 10px 12px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; }
|
||||
input[type="text"]:focus { outline: none; border-color: #0071e3; }
|
||||
.radio-group { display: flex; gap: 16px; margin-top: 6px; }
|
||||
.radio-group label { font-weight: 400; font-size: 14px; display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
||||
.file-input-wrap { margin-top: 8px; }
|
||||
.file-input-wrap input[type="file"] { width: 100%; padding: 8px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; }
|
||||
.hint { font-size: 12px; color: #6e6e73; margin-top: 4px; }
|
||||
.btn { padding: 10px 24px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
|
||||
.btn-primary { background: #0071e3; color: #fff; }
|
||||
.btn-primary:hover { background: #0058b0; }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.progress-wrap { margin-top: 16px; display: none; }
|
||||
.progress-bar { width: 100%; height: 8px; background: #e8e8ed; border-radius: 4px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: #0071e3; width: 0%; transition: width 0.3s; border-radius: 4px; }
|
||||
.progress-text { font-size: 13px; color: #6e6e73; margin-top: 8px; }
|
||||
.result { margin-top: 16px; padding: 12px 16px; border-radius: 8px; font-size: 14px; display: none; }
|
||||
.result.success { background: #d1fae5; color: #065f46; }
|
||||
.result.error { background: #fee2e2; color: #991b1b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="upload-container">
|
||||
<h1>📁 File Upload Service</h1>
|
||||
|
||||
<div class="upload-form">
|
||||
<div class="form-group">
|
||||
<label for="user_id">User ID:</label>
|
||||
<input type="text" id="user_id" value="accusys" placeholder="Enter User ID">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Upload Mode:</label>
|
||||
<div style="margin-top: 10px;">
|
||||
<label style="margin-right: 20px;">
|
||||
<input type="radio" name="upload_mode" value="folder" checked onchange="toggleUploadMode()">
|
||||
📁 Folder Upload (webkitdirectory)
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="upload_mode" value="file" onchange="toggleUploadMode()">
|
||||
📄 Single File Upload (supports ZIP)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="folder-upload-group">
|
||||
<label for="folder">Select Folder:</label>
|
||||
<input type="file" id="folder" multiple webkitdirectory>
|
||||
<p style="color: #666; font-size: 12px; margin-top: 5px;">
|
||||
Upload entire folder with subdirectories
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="file-upload-group" style="display: none;">
|
||||
<label for="file">Select File:</label>
|
||||
<input type="file" id="single_file" accept=".zip,.rar,.7z,.tar,.gz,.bz2,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md,.py,.rs,.js,.ts,.html,.css,.json,.xml,.yaml,.yml,.jpg,.jpeg,.png,.gif,.bmp,.svg,.mp4,.mov,.avi,.mkv,.mp3,.wav,.flac">
|
||||
<p style="color: #666; font-size: 12px; margin-top: 5px;">
|
||||
Supports: ZIP, RAR, 7Z, TAR, PDF, Office, Text, Code, Images, Videos, Audio files
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button onclick="uploadFiles()">Start Upload</button>
|
||||
<div class="container">
|
||||
<h1>Upload</h1>
|
||||
<p class="desc">Upload files to user storage directory</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="form-group">
|
||||
<label for="user_id">User ID</label>
|
||||
<input type="text" id="user_id" value="demo">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Mode</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="mode" value="file" checked onchange="toggleMode()"> Single File</label>
|
||||
<label><input type="radio" name="mode" value="folder" onchange="toggleMode()"> Folder (all files)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="single-group">
|
||||
<div class="file-input-wrap">
|
||||
<input type="file" id="single_file">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="folder-group" style="display:none">
|
||||
<div class="file-input-wrap">
|
||||
<input type="file" id="folder" multiple webkitdirectory>
|
||||
</div>
|
||||
<p class="hint">Uploads all files in the selected folder</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="upload-btn" onclick="uploadFiles()">Upload</button>
|
||||
|
||||
<div class="progress-wrap" id="progress">
|
||||
<div class="progress-bar"><div class="progress-fill" id="progress-fill"></div></div>
|
||||
<div class="progress-text" id="progress-text"></div>
|
||||
</div>
|
||||
<div class="result" id="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleUploadMode() {
|
||||
const mode = document.querySelector('input[name="upload_mode"]:checked').value;
|
||||
const folderGroup = document.getElementById('folder-upload-group');
|
||||
const fileGroup = document.getElementById('file-upload-group');
|
||||
|
||||
if (mode === 'folder') {
|
||||
folderGroup.style.display = 'block';
|
||||
fileGroup.style.display = 'none';
|
||||
} else {
|
||||
folderGroup.style.display = 'none';
|
||||
fileGroup.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFiles() {
|
||||
const userId = document.getElementById('user_id').value.trim();
|
||||
if (!userId) {
|
||||
alert('Please enter User ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadMode = document.querySelector('input[name="upload_mode"]:checked').value;
|
||||
let files;
|
||||
|
||||
if (uploadMode === 'folder') {
|
||||
files = document.getElementById('folder').files;
|
||||
} else {
|
||||
files = document.getElementById('single_file').files;
|
||||
}
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
alert('Please select files or folder');
|
||||
return;
|
||||
}
|
||||
const fileInput = document.getElementById('file');
|
||||
const files = fileInput.files;
|
||||
|
||||
if (!user_id || files.length === 0) {
|
||||
showError('Please enter User ID and select at least one file');
|
||||
return;
|
||||
}
|
||||
|
||||
const progressDiv = document.getElementById('progress');
|
||||
const progressFill = document.getElementById('progress-fill');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
progressDiv.style.display = 'block';
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
let uploaded = 0;
|
||||
const total = files.length;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Create AbortController with timeout (30 minutes for large files)
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
showError(`File ${file.name} upload timeout (30 minutes limit)`);
|
||||
}, 30 * 60 * 1000); // 30 minutes
|
||||
|
||||
try {
|
||||
progressText.textContent = `Uploading: ${file.name} (${uploaded + 1}/${total})`;
|
||||
|
||||
const response = await fetch(`/api/v2/upload-unlimited/${user_id}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId); // Clear timeout if upload succeeds
|
||||
|
||||
// Check HTTP status
|
||||
if (!response.ok) {
|
||||
showError(`File ${file.name} upload failed: HTTP ${response.status} ${response.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check response body
|
||||
const text = await response.text();
|
||||
if (!text || text.trim() === '') {
|
||||
showError(`File ${file.name} upload failed: Server returned empty response`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(text);
|
||||
} catch (parseError) {
|
||||
showError(`File ${file.name} upload failed: JSON parse error - ${parseError.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
uploaded++;
|
||||
const percent = Math.round((uploaded / total) * 100);
|
||||
progressFill.style.width = percent + '%';
|
||||
progressText.textContent = `Upload progress: ${percent}% (${uploaded}/${total})`;
|
||||
} else {
|
||||
showError(`File ${file.name} upload failed: ${result.error || 'Unknown error'}`);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (err.name === 'AbortError') {
|
||||
showError(`File ${file.name} upload timeout (30 minutes limit)`);
|
||||
} else {
|
||||
showError(`File ${file.name} upload error: ${err.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(`Successfully uploaded ${uploaded} files!`);
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.className = 'result success';
|
||||
resultDiv.textContent = message;
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = message;
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
function toggleMode() {
|
||||
const mode = document.querySelector('input[name="mode"]:checked').value;
|
||||
document.getElementById('single-group').style.display = mode === 'file' ? 'block' : 'none';
|
||||
document.getElementById('folder-group').style.display = mode === 'folder' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function uploadFiles() {
|
||||
const uid = document.getElementById('user_id').value.trim();
|
||||
if (!uid) return showError('Enter a user ID');
|
||||
|
||||
const mode = document.querySelector('input[name="mode"]:checked').value;
|
||||
const files = mode === 'folder'
|
||||
? document.getElementById('folder').files
|
||||
: document.getElementById('single_file').files;
|
||||
|
||||
if (!files || files.length === 0) return showError('Select a file or folder');
|
||||
|
||||
const btn = document.getElementById('upload-btn');
|
||||
btn.disabled = true;
|
||||
|
||||
const progress = document.getElementById('progress');
|
||||
const fill = document.getElementById('progress-fill');
|
||||
const ptext = document.getElementById('progress-text');
|
||||
const result = document.getElementById('result');
|
||||
progress.style.display = 'block';
|
||||
result.style.display = 'none';
|
||||
|
||||
let uploaded = 0;
|
||||
const total = files.length;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const f = files[i];
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
ptext.textContent = `Uploading ${f.name} (${i+1}/${total})`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v2/upload-unlimited/${uid}`, { method: 'POST', body: fd });
|
||||
if (!res.ok) { showError(`${f.name}: HTTP ${res.status}`); btn.disabled = false; return; }
|
||||
const data = await res.json();
|
||||
if (!data.ok) { showError(`${f.name}: ${data.error || 'unknown'}`); btn.disabled = false; return; }
|
||||
uploaded++;
|
||||
const pct = Math.round(uploaded / total * 100);
|
||||
fill.style.width = pct + '%';
|
||||
ptext.textContent = `${pct}% (${uploaded}/${total})`;
|
||||
} catch(e) {
|
||||
showError(`${f.name}: ${e.message}`);
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(`Uploaded ${uploaded} file${uploaded > 1 ? 's' : ''}`);
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
function showSuccess(m) { showResult(m, 'success'); }
|
||||
function showError(m) { showResult(m, 'error'); }
|
||||
function showResult(m, t) {
|
||||
const r = document.getElementById('result');
|
||||
r.className = 'result ' + t;
|
||||
r.textContent = m;
|
||||
r.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user