commit 13acc29f020451350fb51e464ee6be3cf1c384b8
parent 66105c90daf7950f1a4938c6324594debbeab9d9
Author: Amit Dutta <mail@amit.is-a.dev>
Date: Fri, 4 Sep 2026 17:09:26 +0530
Merge pull request #14 from notamitgamer/patch
Add upload spinner and improve upload status display
Diffstat:
2 files changed, 48 insertions(+), 7 deletions(-)
diff --git a/app/storage.py b/app/storage.py
@@ -9,15 +9,32 @@ HF_TOKEN = os.getenv("HF_TOKEN")
api = HfApi(token=HF_TOKEN)
+# LRU Cache implementation
_cache = {}
CACHE_TTL = 60
+MAX_CACHE_SIZE = 500
def _get_cache(key):
- if key in _cache and _cache[key][0] > time.time():
- return _cache[key][1]
+ if key in _cache:
+ expiry, value = _cache[key]
+ if expiry > time.time():
+ # Move to the end of the dictionary to mark it as most recently used (LRU)
+ _cache[key] = _cache.pop(key)
+ return value
+ else:
+ # Expired
+ del _cache[key]
return None
def _set_cache(key, value):
+ if key in _cache:
+ # Remove it first so we can re-insert it at the end
+ del _cache[key]
+ elif len(_cache) >= MAX_CACHE_SIZE:
+ # Dictionary is full. Remove the oldest item (which is at the front)
+ oldest_key = next(iter(_cache))
+ del _cache[oldest_key]
+
_cache[key] = (time.time() + CACHE_TTL, value)
async def is_file(path: str) -> bool:
@@ -116,8 +133,14 @@ REPO_STATS_CACHE_TTL = 300
def repo_stats():
cache_key = "repo_stats"
now = time.time()
- if cache_key in _cache and _cache[cache_key][0] > now:
- return _cache[cache_key][1]
+ if cache_key in _cache:
+ expiry, value = _cache[cache_key]
+ if expiry > now:
+ # Move to end as part of LRU update
+ _cache[cache_key] = _cache.pop(cache_key)
+ return value
+ else:
+ del _cache[cache_key]
try:
items = list(api.list_repo_tree(
@@ -135,7 +158,7 @@ def repo_stats():
total_size += getattr(item, "size", 0) or 0
result = {"file_count": file_count, "size_str": format_size(total_size)}
- _cache[cache_key] = (now + REPO_STATS_CACHE_TTL, result)
+ _set_cache(cache_key, result)
return result
def upload_temp_file(temp_path: str, filename: str) -> str:
diff --git a/app/templates/index.html b/app/templates/index.html
@@ -230,6 +230,7 @@
.file-row {
display: flex; justify-content: space-between; padding: 0.75rem;
border: 1px solid var(--border); border-radius: 4px; margin-bottom: 0.5rem;
+ align-items: center;
}
.file-row.done { border-color: #2ea043; }
.file-row.error { border-color: var(--danger); }
@@ -327,6 +328,16 @@
border-color: #ffc9c2;
}
+ .upload-spinner {
+ display: inline-block;
+ width: 12px; height: 12px;
+ border: 2px solid var(--accent-pink);
+ border-top-color: transparent;
+ border-radius: 50%;
+ margin-right: 6px;
+ vertical-align: middle;
+ }
+
footer {
margin-top: 4rem;
padding-top: 1rem;
@@ -591,7 +602,14 @@
xhr.upload.onprogress = (e) => {
if (!e.lengthComputable) return;
const pct = Math.round((e.loaded / e.total) * 100);
- files.forEach(f => { rows[f.name].querySelector('.status').textContent = pct + '%'; });
+ files.forEach(f => {
+ const statusNode = rows[f.name].querySelector('.status');
+ if (pct === 100) {
+ statusNode.innerHTML = `<span class="upload-spinner spin-anim"></span><span style="color: var(--accent-pink);">saving to HF...</span>`;
+ } else {
+ statusNode.textContent = pct + '%';
+ }
+ });
};
xhr.onload = () => {
@@ -771,7 +789,7 @@
if (e.key !== '/') return;
const tag = (document.activeElement && document.activeElement.tagName) || '';
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
- const filterInput = document.getElementById('filter-input');a
+ const filterInput = document.getElementById('filter-input');
if (filterInput) {
e.preventDefault();
filterInput.focus();