Compare commits
35 Commits
f60dbaf9c9
...
v0.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
| f0c95ac0a9 | |||
| 0e392e18b4 | |||
| 8996f1ce06 | |||
| 8ff4797041 | |||
| 50fb5aa387 | |||
| cc161bf362 | |||
| 2a0e77a754 | |||
| eb0e435a5a | |||
| 7633007a08 | |||
| de0d869c9f | |||
| fdd068feee | |||
| 66b7677d2c | |||
| 4d90ead816 | |||
| b37a51ed1d | |||
| 0462a7b62e | |||
| 52660570c1 | |||
| 35f61313e0 | |||
| c470cfb576 | |||
| d96955deee | |||
| 85181f0be6 | |||
| d5ca7a8be1 | |||
| 476dc79e42 | |||
| bb6590fc5e | |||
| 899db3421b | |||
| caf01d6ada | |||
| bb366cb4cd | |||
| a2745ff2ee | |||
| 28cb656d94 | |||
| 3c44152fc6 | |||
| 397515edce | |||
| 980fced7e4 | |||
| bae5009ec4 | |||
| 233780617f | |||
| fd8fb21517 | |||
| c6cbe822e1 |
@@ -175,13 +175,21 @@ def handle_app_error(error: AppError) -> Response:
|
||||
|
||||
def handle_rate_limit_exceeded(e: RateLimitExceeded) -> Response:
|
||||
g.s3_error_code = "SlowDown"
|
||||
if request.path.startswith("/ui") or request.path.startswith("/buckets"):
|
||||
wants_json = (
|
||||
request.is_json or
|
||||
request.headers.get("X-Requested-With") == "XMLHttpRequest" or
|
||||
"application/json" in request.accept_mimetypes.values()
|
||||
)
|
||||
if wants_json:
|
||||
return jsonify({"success": False, "error": {"code": "SlowDown", "message": "Please reduce your request rate."}}), 429
|
||||
error = Element("Error")
|
||||
SubElement(error, "Code").text = "SlowDown"
|
||||
SubElement(error, "Message").text = "Please reduce your request rate."
|
||||
SubElement(error, "Resource").text = request.path
|
||||
SubElement(error, "RequestId").text = getattr(g, "request_id", "")
|
||||
xml_bytes = tostring(error, encoding="utf-8")
|
||||
return Response(xml_bytes, status=429, mimetype="application/xml")
|
||||
return Response(xml_bytes, status="429 Too Many Requests", mimetype="application/xml")
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
|
||||
39
app/ui.py
39
app/ui.py
@@ -1063,6 +1063,27 @@ def bulk_delete_objects(bucket_name: str):
|
||||
return _respond(False, f"A maximum of {MAX_KEYS} objects can be deleted per request", status_code=400)
|
||||
|
||||
unique_keys = list(dict.fromkeys(cleaned))
|
||||
|
||||
folder_prefixes = [k for k in unique_keys if k.endswith("/")]
|
||||
if folder_prefixes:
|
||||
try:
|
||||
client = get_session_s3_client()
|
||||
for prefix in folder_prefixes:
|
||||
unique_keys.remove(prefix)
|
||||
paginator = client.get_paginator("list_objects_v2")
|
||||
for page in paginator.paginate(Bucket=bucket_name, Prefix=prefix):
|
||||
for obj in page.get("Contents", []):
|
||||
if obj["Key"] not in unique_keys:
|
||||
unique_keys.append(obj["Key"])
|
||||
except (ClientError, EndpointConnectionError, ConnectionClosedError) as exc:
|
||||
if isinstance(exc, ClientError):
|
||||
err, status = handle_client_error(exc)
|
||||
return _respond(False, err["error"], status_code=status)
|
||||
return _respond(False, "S3 API server is unreachable", status_code=502)
|
||||
|
||||
if not unique_keys:
|
||||
return _respond(False, "No objects found under the selected folders", status_code=400)
|
||||
|
||||
try:
|
||||
_authorize_ui(principal, bucket_name, "delete")
|
||||
except IamError as exc:
|
||||
@@ -1093,13 +1114,17 @@ def bulk_delete_objects(bucket_name: str):
|
||||
else:
|
||||
try:
|
||||
client = get_session_s3_client()
|
||||
objects_to_delete = [{"Key": k} for k in unique_keys]
|
||||
resp = client.delete_objects(
|
||||
Bucket=bucket_name,
|
||||
Delete={"Objects": objects_to_delete, "Quiet": False},
|
||||
)
|
||||
deleted = [d["Key"] for d in resp.get("Deleted", [])]
|
||||
errors = [{"key": e["Key"], "error": e.get("Message", e.get("Code", "Unknown error"))} for e in resp.get("Errors", [])]
|
||||
deleted = []
|
||||
errors = []
|
||||
for i in range(0, len(unique_keys), 1000):
|
||||
batch = unique_keys[i:i + 1000]
|
||||
objects_to_delete = [{"Key": k} for k in batch]
|
||||
resp = client.delete_objects(
|
||||
Bucket=bucket_name,
|
||||
Delete={"Objects": objects_to_delete, "Quiet": False},
|
||||
)
|
||||
deleted.extend(d["Key"] for d in resp.get("Deleted", []))
|
||||
errors.extend({"key": e["Key"], "error": e.get("Message", e.get("Code", "Unknown error"))} for e in resp.get("Errors", []))
|
||||
for key in deleted:
|
||||
_replication_manager().trigger_replication(bucket_name, key, action="delete")
|
||||
except (ClientError, EndpointConnectionError, ConnectionClosedError) as exc:
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
const previewMetadataList = document.getElementById('preview-metadata-list');
|
||||
const previewPlaceholder = document.getElementById('preview-placeholder');
|
||||
const previewPlaceholderDefault = previewPlaceholder ? previewPlaceholder.innerHTML : '';
|
||||
const previewErrorAlert = document.getElementById('preview-error-alert');
|
||||
const previewDetailsMeta = document.getElementById('preview-details-meta');
|
||||
const previewImage = document.getElementById('preview-image');
|
||||
const previewVideo = document.getElementById('preview-video');
|
||||
const previewAudio = document.getElementById('preview-audio');
|
||||
@@ -867,6 +869,11 @@
|
||||
const checkbox = row.querySelector('[data-folder-select]');
|
||||
checkbox?.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
if (checkbox.checked) {
|
||||
selectedRows.set(folderPath, { key: folderPath, isFolder: true });
|
||||
} else {
|
||||
selectedRows.delete(folderPath);
|
||||
}
|
||||
const folderObjects = allObjects.filter(obj => obj.key.startsWith(folderPath));
|
||||
folderObjects.forEach(obj => {
|
||||
if (checkbox.checked) {
|
||||
@@ -1351,8 +1358,11 @@
|
||||
}
|
||||
if (selectAllCheckbox) {
|
||||
const filesInView = visibleItems.filter(item => item.type === 'file');
|
||||
const total = filesInView.length;
|
||||
const visibleSelectedCount = filesInView.filter(item => selectedRows.has(item.data.key)).length;
|
||||
const foldersInView = visibleItems.filter(item => item.type === 'folder');
|
||||
const total = filesInView.length + foldersInView.length;
|
||||
const fileSelectedCount = filesInView.filter(item => selectedRows.has(item.data.key)).length;
|
||||
const folderSelectedCount = foldersInView.filter(item => selectedRows.has(item.path)).length;
|
||||
const visibleSelectedCount = fileSelectedCount + folderSelectedCount;
|
||||
selectAllCheckbox.disabled = total === 0;
|
||||
selectAllCheckbox.checked = visibleSelectedCount > 0 && visibleSelectedCount === total && total > 0;
|
||||
selectAllCheckbox.indeterminate = visibleSelectedCount > 0 && visibleSelectedCount < total;
|
||||
@@ -1374,8 +1384,12 @@
|
||||
const keys = Array.from(selectedRows.keys());
|
||||
bulkDeleteList.innerHTML = '';
|
||||
if (bulkDeleteCount) {
|
||||
const label = keys.length === 1 ? 'object' : 'objects';
|
||||
bulkDeleteCount.textContent = `${keys.length} ${label} selected`;
|
||||
const folderCount = keys.filter(k => k.endsWith('/')).length;
|
||||
const objectCount = keys.length - folderCount;
|
||||
const parts = [];
|
||||
if (folderCount) parts.push(`${folderCount} folder${folderCount !== 1 ? 's' : ''}`);
|
||||
if (objectCount) parts.push(`${objectCount} object${objectCount !== 1 ? 's' : ''}`);
|
||||
bulkDeleteCount.textContent = `${parts.join(' and ')} selected`;
|
||||
}
|
||||
if (!keys.length) {
|
||||
const empty = document.createElement('li');
|
||||
@@ -1978,6 +1992,34 @@
|
||||
previewPlaceholder.classList.remove('d-none');
|
||||
};
|
||||
|
||||
let previewFailed = false;
|
||||
|
||||
const handlePreviewError = () => {
|
||||
previewFailed = true;
|
||||
if (downloadButton) {
|
||||
downloadButton.classList.add('disabled');
|
||||
downloadButton.removeAttribute('href');
|
||||
}
|
||||
if (presignButton) presignButton.disabled = true;
|
||||
if (generatePresignButton) generatePresignButton.disabled = true;
|
||||
if (previewDetailsMeta) previewDetailsMeta.classList.add('d-none');
|
||||
if (previewMetadata) previewMetadata.classList.add('d-none');
|
||||
const tagsPanel = document.getElementById('preview-tags');
|
||||
if (tagsPanel) tagsPanel.classList.add('d-none');
|
||||
const versionPanel = document.getElementById('version-panel');
|
||||
if (versionPanel) versionPanel.classList.add('d-none');
|
||||
if (previewErrorAlert) {
|
||||
previewErrorAlert.textContent = 'Unable to load object \u2014 it may have been deleted, or the server returned an error.';
|
||||
previewErrorAlert.classList.remove('d-none');
|
||||
}
|
||||
};
|
||||
|
||||
const clearPreviewError = () => {
|
||||
previewFailed = false;
|
||||
if (previewErrorAlert) previewErrorAlert.classList.add('d-none');
|
||||
if (previewDetailsMeta) previewDetailsMeta.classList.remove('d-none');
|
||||
};
|
||||
|
||||
async function fetchMetadata(metadataUrl) {
|
||||
if (!metadataUrl) return null;
|
||||
try {
|
||||
@@ -1999,6 +2041,7 @@
|
||||
previewPanel.classList.remove('d-none');
|
||||
activeRow = row;
|
||||
renderMetadata(null);
|
||||
clearPreviewError();
|
||||
|
||||
previewKey.textContent = row.dataset.key;
|
||||
previewSize.textContent = formatBytes(Number(row.dataset.size));
|
||||
@@ -2024,25 +2067,69 @@
|
||||
if (previewUrl && lower.match(/\.(png|jpg|jpeg|gif|webp|svg|ico|bmp)$/)) {
|
||||
previewPlaceholder.innerHTML = '<div class="spinner-border spinner-border-sm text-secondary" role="status"></div><div class="small mt-2">Loading preview\u2026</div>';
|
||||
const currentRow = row;
|
||||
previewImage.onload = () => {
|
||||
if (activeRow !== currentRow) return;
|
||||
previewImage.classList.remove('d-none');
|
||||
previewPlaceholder.classList.add('d-none');
|
||||
};
|
||||
previewImage.onerror = () => {
|
||||
if (activeRow !== currentRow) return;
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
};
|
||||
previewImage.src = previewUrl;
|
||||
fetch(previewUrl)
|
||||
.then((r) => {
|
||||
if (activeRow !== currentRow) return;
|
||||
if (!r.ok) {
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
handlePreviewError();
|
||||
return;
|
||||
}
|
||||
return r.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
if (!blob || activeRow !== currentRow) return;
|
||||
const url = URL.createObjectURL(blob);
|
||||
previewImage.onload = () => {
|
||||
if (activeRow !== currentRow) { URL.revokeObjectURL(url); return; }
|
||||
previewImage.classList.remove('d-none');
|
||||
previewPlaceholder.classList.add('d-none');
|
||||
};
|
||||
previewImage.onerror = () => {
|
||||
if (activeRow !== currentRow) { URL.revokeObjectURL(url); return; }
|
||||
URL.revokeObjectURL(url);
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
};
|
||||
previewImage.src = url;
|
||||
})
|
||||
.catch(() => {
|
||||
if (activeRow !== currentRow) return;
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
handlePreviewError();
|
||||
});
|
||||
} else if (previewUrl && lower.match(/\.(mp4|webm|ogv|mov|avi|mkv)$/)) {
|
||||
const currentRow = row;
|
||||
previewVideo.onerror = () => {
|
||||
if (activeRow !== currentRow) return;
|
||||
previewVideo.classList.add('d-none');
|
||||
previewPlaceholder.classList.remove('d-none');
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
handlePreviewError();
|
||||
};
|
||||
previewVideo.src = previewUrl;
|
||||
previewVideo.classList.remove('d-none');
|
||||
previewPlaceholder.classList.add('d-none');
|
||||
} else if (previewUrl && lower.match(/\.(mp3|wav|flac|ogg|aac|m4a|wma)$/)) {
|
||||
const currentRow = row;
|
||||
previewAudio.onerror = () => {
|
||||
if (activeRow !== currentRow) return;
|
||||
previewAudio.classList.add('d-none');
|
||||
previewPlaceholder.classList.remove('d-none');
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
handlePreviewError();
|
||||
};
|
||||
previewAudio.src = previewUrl;
|
||||
previewAudio.classList.remove('d-none');
|
||||
previewPlaceholder.classList.add('d-none');
|
||||
} else if (previewUrl && lower.match(/\.(pdf)$/)) {
|
||||
const currentRow = row;
|
||||
previewIframe.onerror = () => {
|
||||
if (activeRow !== currentRow) return;
|
||||
previewIframe.classList.add('d-none');
|
||||
previewPlaceholder.classList.remove('d-none');
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
handlePreviewError();
|
||||
};
|
||||
previewIframe.src = previewUrl;
|
||||
previewIframe.style.minHeight = '500px';
|
||||
previewIframe.classList.remove('d-none');
|
||||
@@ -2067,14 +2154,17 @@
|
||||
})
|
||||
.catch(() => {
|
||||
if (activeRow !== currentRow) return;
|
||||
previewText.textContent = 'Failed to load preview';
|
||||
previewText.classList.add('d-none');
|
||||
previewPlaceholder.classList.remove('d-none');
|
||||
previewPlaceholder.innerHTML = '<div class="small text-muted">Failed to load preview</div>';
|
||||
handlePreviewError();
|
||||
});
|
||||
}
|
||||
|
||||
const metadataUrl = row.dataset.metadataUrl;
|
||||
if (metadataUrl) {
|
||||
const metadata = await fetchMetadata(metadataUrl);
|
||||
if (activeRow === row) {
|
||||
if (activeRow === row && !previewFailed) {
|
||||
renderMetadata(metadata);
|
||||
}
|
||||
}
|
||||
@@ -3172,6 +3262,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
const foldersInView = visibleItems.filter(item => item.type === 'folder');
|
||||
foldersInView.forEach(item => {
|
||||
if (shouldSelect) {
|
||||
selectedRows.set(item.path, { key: item.path, isFolder: true });
|
||||
} else {
|
||||
selectedRows.delete(item.path);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-folder-select]').forEach(cb => {
|
||||
cb.checked = shouldSelect;
|
||||
});
|
||||
@@ -3972,6 +4071,10 @@
|
||||
|
||||
const loadObjectTags = async (row) => {
|
||||
if (!row || !previewTagsPanel) return;
|
||||
if (previewFailed) {
|
||||
previewTagsPanel.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
const tagsUrl = row.dataset.tagsUrl;
|
||||
if (!tagsUrl) {
|
||||
previewTagsPanel.classList.add('d-none');
|
||||
|
||||
@@ -257,7 +257,8 @@
|
||||
Share Link
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-3 rounded mb-3" style="background: var(--myfsio-preview-bg);">
|
||||
<div id="preview-error-alert" class="alert alert-warning d-none py-2 px-3 mb-3 small" role="alert"></div>
|
||||
<div id="preview-details-meta" class="p-3 rounded mb-3" style="background: var(--myfsio-preview-bg);">
|
||||
<dl class="row small mb-0">
|
||||
<dt class="col-5 text-muted fw-normal">Last modified</dt>
|
||||
<dd class="col-7 mb-2 fw-medium" id="preview-modified"></dd>
|
||||
|
||||
Reference in New Issue
Block a user