Spaces:
Sleeping
Sleeping
Commit ·
5ae5432
1
Parent(s): 8354653
Raise library GeoTIFF upload limit to 5 GB on dev.
Browse filesDefault MAX_GEOTIFF_MB to 5120, stream uploads with GB-aware errors,
client-side size check, and longer keep-alive for large file transfers.
Co-authored-by: Cursor <cursoragent@cursor.com>
- DEPLOYMENT.md +1 -1
- Dockerfile +2 -1
- app/dda/config.py +12 -2
- app/dda/library_routes.py +1 -0
- app/dda/local_routes.py +91 -2
- app/dda/upload_io.py +3 -1
- library_sources/README.md +1 -1
- run.py +7 -1
- static/js/dda/app.js +15 -0
- static/js/dda/library.js +7 -0
DEPLOYMENT.md
CHANGED
|
@@ -134,7 +134,7 @@ Set these in each Space’s **Settings → Repository secrets / Variables** if n
|
|
| 134 |
|----------|---------|
|
| 135 |
| `APP_MODE` | Set to `dda` on **satdetect-dev** only (enables DDA library UI) |
|
| 136 |
| `LOCAL_LIBRARY_ROOT` | Path to year folders (default: `library_sources/` in project) |
|
| 137 |
-
| `MAX_GEOTIFF_MB` | Library GeoTIFF upload cap (default **
|
| 138 |
| `MAX_IMAGE_MB` | PNG/JPEG library cap (default 50 MB) |
|
| 139 |
| `SECRET_KEY` | Optional legacy JWT setting (login disabled) |
|
| 140 |
| `DATABASE_URL` | PostgreSQL instead of SQLite (optional) |
|
|
|
|
| 134 |
|----------|---------|
|
| 135 |
| `APP_MODE` | Set to `dda` on **satdetect-dev** only (enables DDA library UI) |
|
| 136 |
| `LOCAL_LIBRARY_ROOT` | Path to year folders (default: `library_sources/` in project) |
|
| 137 |
+
| `MAX_GEOTIFF_MB` | Library GeoTIFF upload cap (default **5120** = 5 GB on dev) |
|
| 138 |
| `MAX_IMAGE_MB` | PNG/JPEG library cap (default 50 MB) |
|
| 139 |
| `SECRET_KEY` | Optional legacy JWT setting (login disabled) |
|
| 140 |
| `DATABASE_URL` | PostgreSQL instead of SQLite (optional) |
|
Dockerfile
CHANGED
|
@@ -21,7 +21,8 @@ WORKDIR /app
|
|
| 21 |
|
| 22 |
# Build-time info + cache-bust:
|
| 23 |
# Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
|
| 24 |
-
ARG APP_BUILD=
|
|
|
|
| 25 |
ENV APP_BUILD=${APP_BUILD}
|
| 26 |
ENV GDAL_CONFIG=/usr/bin/gdal-config
|
| 27 |
RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
|
|
|
|
| 21 |
|
| 22 |
# Build-time info + cache-bust:
|
| 23 |
# Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
|
| 24 |
+
ARG APP_BUILD=30
|
| 25 |
+
ENV MAX_GEOTIFF_MB=5120
|
| 26 |
ENV APP_BUILD=${APP_BUILD}
|
| 27 |
ENV GDAL_CONFIG=/usr/bin/gdal-config
|
| 28 |
RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
|
app/dda/config.py
CHANGED
|
@@ -58,8 +58,8 @@ THUMBS_DIR = LIBRARY_DIR / "thumbs"
|
|
| 58 |
PREVIEWS_DIR = LIBRARY_DIR / "previews"
|
| 59 |
LOCAL_THUMB_CACHE = DATA_DIR / "library_cache" / "thumbs"
|
| 60 |
|
| 61 |
-
# GeoTIFF library upload limit (default
|
| 62 |
-
MAX_GEOTIFF_BYTES = int(os.environ.get("MAX_GEOTIFF_MB", "
|
| 63 |
|
| 64 |
# Raster sidecar formats (PNG/JPEG) — smaller cap for library uploads
|
| 65 |
MAX_IMAGE_BYTES = int(os.environ.get("MAX_IMAGE_MB", "50")) * 1024 * 1024
|
|
@@ -104,3 +104,13 @@ def geotiff_io_available() -> bool:
|
|
| 104 |
return True
|
| 105 |
except ImportError:
|
| 106 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
PREVIEWS_DIR = LIBRARY_DIR / "previews"
|
| 59 |
LOCAL_THUMB_CACHE = DATA_DIR / "library_cache" / "thumbs"
|
| 60 |
|
| 61 |
+
# GeoTIFF library upload limit (default 5 GB; override with MAX_GEOTIFF_MB on HF dev Space)
|
| 62 |
+
MAX_GEOTIFF_BYTES = int(os.environ.get("MAX_GEOTIFF_MB", "5120")) * 1024 * 1024
|
| 63 |
|
| 64 |
# Raster sidecar formats (PNG/JPEG) — smaller cap for library uploads
|
| 65 |
MAX_IMAGE_BYTES = int(os.environ.get("MAX_IMAGE_MB", "50")) * 1024 * 1024
|
|
|
|
| 104 |
return True
|
| 105 |
except ImportError:
|
| 106 |
return False
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def get_detection_max_side() -> int:
|
| 110 |
+
"""Max pixel dimension for GeoTIFF load + detection pipeline (higher = sharper, more RAM)."""
|
| 111 |
+
default = "2048" if is_hf_hosted() else "4096"
|
| 112 |
+
try:
|
| 113 |
+
value = int(os.environ.get("DETECTION_MAX_SIDE", default))
|
| 114 |
+
except ValueError:
|
| 115 |
+
value = int(default)
|
| 116 |
+
return max(1024, min(8192, value))
|
app/dda/library_routes.py
CHANGED
|
@@ -72,6 +72,7 @@ def dda_config():
|
|
| 72 |
"maxUploadMb": MAX_GEOTIFF_BYTES // (1024 * 1024),
|
| 73 |
"maxUploadGb": round(max_gb, 2),
|
| 74 |
"maxGeotiffMb": MAX_GEOTIFF_BYTES // (1024 * 1024),
|
|
|
|
| 75 |
"maxImageMb": MAX_IMAGE_BYTES // (1024 * 1024),
|
| 76 |
"geotiffEnabled": geotiff_io_available(),
|
| 77 |
"allowedExtensions": sorted(ALLOWED_EXTENSIONS),
|
|
|
|
| 72 |
"maxUploadMb": MAX_GEOTIFF_BYTES // (1024 * 1024),
|
| 73 |
"maxUploadGb": round(max_gb, 2),
|
| 74 |
"maxGeotiffMb": MAX_GEOTIFF_BYTES // (1024 * 1024),
|
| 75 |
+
"maxGeotiffBytes": MAX_GEOTIFF_BYTES,
|
| 76 |
"maxImageMb": MAX_IMAGE_BYTES // (1024 * 1024),
|
| 77 |
"geotiffEnabled": geotiff_io_available(),
|
| 78 |
"allowedExtensions": sorted(ALLOWED_EXTENSIONS),
|
app/dda/local_routes.py
CHANGED
|
@@ -3,12 +3,20 @@ import logging
|
|
| 3 |
from pathlib import Path
|
| 4 |
from typing import Optional
|
| 5 |
|
| 6 |
-
from fastapi import APIRouter, File, Form, HTTPException, Query, UploadFile
|
| 7 |
from fastapi.responses import FileResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
from .config import (
|
| 10 |
IS_DDA_MODE,
|
|
|
|
| 11 |
geotiff_io_available,
|
|
|
|
| 12 |
get_library_roots,
|
| 13 |
get_writable_library_root,
|
| 14 |
is_hf_hosted,
|
|
@@ -69,6 +77,10 @@ def local_library_config():
|
|
| 69 |
"writablePath": writable,
|
| 70 |
"instructions": instructions,
|
| 71 |
"geotiffEnabled": geotiff_io_available(),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
|
|
@@ -117,7 +129,13 @@ def local_thumb(path: str = Query(...)):
|
|
| 117 |
except HTTPException:
|
| 118 |
raise
|
| 119 |
except Exception as exc:
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
|
| 123 |
@router.post("/local/upload")
|
|
@@ -177,3 +195,74 @@ def local_rescan():
|
|
| 177 |
"writablePath": str(get_writable_library_root()),
|
| 178 |
"debug": info,
|
| 179 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
from typing import Optional
|
| 5 |
|
| 6 |
+
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
|
| 7 |
from fastapi.responses import FileResponse
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from sqlalchemy.orm import Session
|
| 10 |
+
|
| 11 |
+
from ..database import get_db
|
| 12 |
+
from .detect_service import run_detection_and_save
|
| 13 |
+
from .geotiff_io import load_rgb_pil
|
| 14 |
|
| 15 |
from .config import (
|
| 16 |
IS_DDA_MODE,
|
| 17 |
+
MAX_GEOTIFF_BYTES,
|
| 18 |
geotiff_io_available,
|
| 19 |
+
get_detection_max_side,
|
| 20 |
get_library_roots,
|
| 21 |
get_writable_library_root,
|
| 22 |
is_hf_hosted,
|
|
|
|
| 77 |
"writablePath": writable,
|
| 78 |
"instructions": instructions,
|
| 79 |
"geotiffEnabled": geotiff_io_available(),
|
| 80 |
+
"detectionMaxSide": get_detection_max_side(),
|
| 81 |
+
"maxGeotiffMb": MAX_GEOTIFF_BYTES // (1024 * 1024),
|
| 82 |
+
"maxGeotiffBytes": MAX_GEOTIFF_BYTES,
|
| 83 |
+
"maxUploadGb": round(MAX_GEOTIFF_BYTES / (1024 ** 3), 2),
|
| 84 |
}
|
| 85 |
|
| 86 |
|
|
|
|
| 129 |
except HTTPException:
|
| 130 |
raise
|
| 131 |
except Exception as exc:
|
| 132 |
+
logger.warning("Thumb endpoint fallback for %s: %s", path, exc)
|
| 133 |
+
from .config import LOCAL_THUMB_CACHE
|
| 134 |
+
from .geotiff_io import write_placeholder_png
|
| 135 |
+
|
| 136 |
+
cache = LOCAL_THUMB_CACHE / "fallback.png"
|
| 137 |
+
write_placeholder_png(cache, Path(path).name)
|
| 138 |
+
return FileResponse(cache, media_type="image/png")
|
| 139 |
|
| 140 |
|
| 141 |
@router.post("/local/upload")
|
|
|
|
| 195 |
"writablePath": str(get_writable_library_root()),
|
| 196 |
"debug": info,
|
| 197 |
}
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@router.post("/detect/from-library")
|
| 201 |
+
async def detect_from_library(
|
| 202 |
+
base_path: str = Form(...),
|
| 203 |
+
comparison_path: str = Form(...),
|
| 204 |
+
method: str = Form("AI-Based Deep Learning"),
|
| 205 |
+
title: str = Form("Untitled run"),
|
| 206 |
+
zone: str = Form(""),
|
| 207 |
+
village: str = Form(""),
|
| 208 |
+
enable_registration: bool = Form(True),
|
| 209 |
+
enable_normalization: bool = Form(True),
|
| 210 |
+
detection_sensitivity: float = Form(0.5),
|
| 211 |
+
min_region_area: Optional[int] = Form(150),
|
| 212 |
+
notify_email: Optional[str] = Form(None),
|
| 213 |
+
db: Session = Depends(get_db),
|
| 214 |
+
):
|
| 215 |
+
"""Run change detection on two library images by relative path (e.g. 2025/aerial.tif)."""
|
| 216 |
+
_require_dda()
|
| 217 |
+
base_norm = base_path.replace("\\", "/").strip()
|
| 218 |
+
comp_norm = comparison_path.replace("\\", "/").strip()
|
| 219 |
+
if not base_norm or not comp_norm:
|
| 220 |
+
raise HTTPException(status_code=400, detail="base_path and comparison_path are required")
|
| 221 |
+
if base_norm == comp_norm:
|
| 222 |
+
raise HTTPException(status_code=400, detail="Base and comparison images must be different")
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
base_file = safe_resolve(base_norm)
|
| 226 |
+
comp_file = safe_resolve(comp_norm)
|
| 227 |
+
except HTTPException:
|
| 228 |
+
raise
|
| 229 |
+
except Exception as exc:
|
| 230 |
+
raise HTTPException(status_code=400, detail=f"Invalid library path: {exc}") from exc
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
before_pil = load_rgb_pil(base_file, max_side=get_detection_max_side())
|
| 234 |
+
after_pil = load_rgb_pil(comp_file, max_side=get_detection_max_side())
|
| 235 |
+
except RuntimeError as exc:
|
| 236 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 237 |
+
except Exception as exc:
|
| 238 |
+
raise HTTPException(status_code=400, detail=f"Could not load images: {exc}") from exc
|
| 239 |
+
|
| 240 |
+
# Match dimensions so registration and overlay align with the before image
|
| 241 |
+
if before_pil.size != after_pil.size:
|
| 242 |
+
after_pil = after_pil.resize(before_pil.size, Image.Resampling.LANCZOS)
|
| 243 |
+
|
| 244 |
+
max_side = get_detection_max_side()
|
| 245 |
+
|
| 246 |
+
if title == "Untitled run":
|
| 247 |
+
title = f"{Path(base_norm).name} vs {Path(comp_norm).name}"
|
| 248 |
+
|
| 249 |
+
try:
|
| 250 |
+
return run_detection_and_save(
|
| 251 |
+
db,
|
| 252 |
+
before_pil,
|
| 253 |
+
after_pil,
|
| 254 |
+
method=method,
|
| 255 |
+
title=title,
|
| 256 |
+
zone=zone,
|
| 257 |
+
village=village,
|
| 258 |
+
enable_registration=enable_registration,
|
| 259 |
+
enable_normalization=enable_normalization,
|
| 260 |
+
detection_sensitivity=detection_sensitivity,
|
| 261 |
+
min_region_area=min_region_area,
|
| 262 |
+
notify_email=notify_email,
|
| 263 |
+
max_size=max_side,
|
| 264 |
+
geo_bounds_path=base_file,
|
| 265 |
+
)
|
| 266 |
+
except Exception as exc:
|
| 267 |
+
logger.exception("Library detection failed for %s vs %s", base_norm, comp_norm)
|
| 268 |
+
raise HTTPException(status_code=500, detail=f"Detection failed: {exc}") from exc
|
app/dda/upload_io.py
CHANGED
|
@@ -30,9 +30,11 @@ async def stream_upload_to_file(
|
|
| 30 |
break
|
| 31 |
total += len(chunk)
|
| 32 |
if total > max_bytes:
|
|
|
|
|
|
|
| 33 |
raise HTTPException(
|
| 34 |
status_code=400,
|
| 35 |
-
detail=f"File too large (max {
|
| 36 |
)
|
| 37 |
out.write(chunk)
|
| 38 |
except HTTPException:
|
|
|
|
| 30 |
break
|
| 31 |
total += len(chunk)
|
| 32 |
if total > max_bytes:
|
| 33 |
+
max_mb = max_bytes // (1024 * 1024)
|
| 34 |
+
limit = f"{max_mb // 1024} GB" if max_mb >= 1024 else f"{max_mb} MB"
|
| 35 |
raise HTTPException(
|
| 36 |
status_code=400,
|
| 37 |
+
detail=f"File too large (max {limit})",
|
| 38 |
)
|
| 39 |
out.write(chunk)
|
| 40 |
except HTTPException:
|
library_sources/README.md
CHANGED
|
@@ -44,7 +44,7 @@ library_sources/
|
|
| 44 |
|
| 45 |
## Large files
|
| 46 |
|
| 47 |
-
GeoTIFF files up to **
|
| 48 |
|
| 49 |
## Custom location
|
| 50 |
|
|
|
|
| 44 |
|
| 45 |
## Large files
|
| 46 |
|
| 47 |
+
GeoTIFF files up to **5 GB** are supported when read from disk. Copy files via Explorer/Finder — much faster than browser upload.
|
| 48 |
|
| 49 |
## Custom location
|
| 50 |
|
run.py
CHANGED
|
@@ -54,7 +54,13 @@ def main():
|
|
| 54 |
|
| 55 |
# reload=False keeps it simple and IDLE-friendly. For live-reload during
|
| 56 |
# development, run instead: uvicorn app.main:app --reload --port 8000
|
| 57 |
-
uvicorn.run(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
if __name__ == "__main__":
|
|
|
|
| 54 |
|
| 55 |
# reload=False keeps it simple and IDLE-friendly. For live-reload during
|
| 56 |
# development, run instead: uvicorn app.main:app --reload --port 8000
|
| 57 |
+
uvicorn.run(
|
| 58 |
+
"app.main:app",
|
| 59 |
+
host=HOST,
|
| 60 |
+
port=PORT,
|
| 61 |
+
reload=False,
|
| 62 |
+
timeout_keep_alive=600,
|
| 63 |
+
)
|
| 64 |
|
| 65 |
|
| 66 |
if __name__ == "__main__":
|
static/js/dda/app.js
CHANGED
|
@@ -57,6 +57,9 @@ document.querySelectorAll('.dda-tab').forEach((btn) => {
|
|
| 57 |
btn.classList.add('active');
|
| 58 |
const tab = btn.dataset.tab;
|
| 59 |
document.getElementById('tab-' + tab)?.classList.add('active');
|
|
|
|
|
|
|
|
|
|
| 60 |
});
|
| 61 |
});
|
| 62 |
|
|
@@ -100,6 +103,16 @@ async function initDda() {
|
|
| 100 |
const hfUpload = document.getElementById('hf-upload-card');
|
| 101 |
if (hfUpload) hfUpload.classList.toggle('hidden', !localCfg.isHosted);
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
if (!localCfg.isHosted && ddaConfig.appMode !== 'dda') {
|
| 104 |
showDdaError('DDA mode is off. Run locally with: python run.py');
|
| 105 |
}
|
|
@@ -126,6 +139,7 @@ async function loadLibraryImages() {
|
|
| 126 |
}
|
| 127 |
try {
|
| 128 |
const items = await ddaApi('GET', '/api/dda/local/images?' + params.toString());
|
|
|
|
| 129 |
if (!items.length) {
|
| 130 |
const hf = window.ddaState?.localCfg?.isHosted;
|
| 131 |
grid.innerHTML = hf
|
|
@@ -151,6 +165,7 @@ async function loadLibraryImages() {
|
|
| 151 |
e.dataTransfer.setData('text/plain', card.dataset.imagePath);
|
| 152 |
});
|
| 153 |
});
|
|
|
|
| 154 |
} catch (err) {
|
| 155 |
grid.innerHTML = `<p class="dim">Could not load images: ${err.message}</p>`;
|
| 156 |
}
|
|
|
|
| 57 |
btn.classList.add('active');
|
| 58 |
const tab = btn.dataset.tab;
|
| 59 |
document.getElementById('tab-' + tab)?.classList.add('active');
|
| 60 |
+
if (tab === 'detect' && typeof loadCompareLibraryGrid === 'function') {
|
| 61 |
+
loadCompareLibraryGrid();
|
| 62 |
+
}
|
| 63 |
});
|
| 64 |
});
|
| 65 |
|
|
|
|
| 103 |
const hfUpload = document.getElementById('hf-upload-card');
|
| 104 |
if (hfUpload) hfUpload.classList.toggle('hidden', !localCfg.isHosted);
|
| 105 |
|
| 106 |
+
const resHint = document.getElementById('dda-detect-res-hint');
|
| 107 |
+
if (resHint && localCfg.detectionMaxSide) {
|
| 108 |
+
resHint.textContent = `Detection runs at up to ${localCfg.detectionMaxSide}px per side for sharper results (set DETECTION_MAX_SIDE to change).`;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
const uploadLimit = document.getElementById('hf-upload-limit');
|
| 112 |
+
if (uploadLimit && localCfg.maxUploadGb) {
|
| 113 |
+
uploadLimit.textContent = `Files on your computer are not on the server. Upload .tif images here (up to ${localCfg.maxUploadGb} GB each).`;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
if (!localCfg.isHosted && ddaConfig.appMode !== 'dda') {
|
| 117 |
showDdaError('DDA mode is off. Run locally with: python run.py');
|
| 118 |
}
|
|
|
|
| 139 |
}
|
| 140 |
try {
|
| 141 |
const items = await ddaApi('GET', '/api/dda/local/images?' + params.toString());
|
| 142 |
+
window.ddaState.libraryItems = items;
|
| 143 |
if (!items.length) {
|
| 144 |
const hf = window.ddaState?.localCfg?.isHosted;
|
| 145 |
grid.innerHTML = hf
|
|
|
|
| 165 |
e.dataTransfer.setData('text/plain', card.dataset.imagePath);
|
| 166 |
});
|
| 167 |
});
|
| 168 |
+
if (typeof loadCompareLibraryGrid === 'function') loadCompareLibraryGrid();
|
| 169 |
} catch (err) {
|
| 170 |
grid.innerHTML = `<p class="dim">Could not load images: ${err.message}</p>`;
|
| 171 |
}
|
static/js/dda/library.js
CHANGED
|
@@ -67,6 +67,13 @@ document.getElementById('form-hf-upload')?.addEventListener('submit', async (e)
|
|
| 67 |
return;
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
const form = new FormData();
|
| 71 |
form.append('file', file);
|
| 72 |
form.append('year', document.getElementById('hf-year').value);
|
|
|
|
| 67 |
return;
|
| 68 |
}
|
| 69 |
|
| 70 |
+
const maxBytes = window.ddaState?.localCfg?.maxGeotiffBytes
|
| 71 |
+
|| (window.ddaState?.localCfg?.maxGeotiffMb || 5120) * 1024 * 1024;
|
| 72 |
+
if (file.size > maxBytes) {
|
| 73 |
+
showDdaError?.(`File is ${formatBytes(file.size)} — maximum upload size is ${formatBytes(maxBytes)}.`);
|
| 74 |
+
return;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
const form = new FormData();
|
| 78 |
form.append('file', file);
|
| 79 |
form.append('year', document.getElementById('hf-year').value);
|