option to keep all activities private from strava zip, fix copy of register link
This commit is contained in:
@@ -786,7 +786,10 @@ async def strava_reset(request: Request) -> JSONResponse:
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/api/upload/strava-zip")
|
@app.post("/api/upload/strava-zip")
|
||||||
async def upload_strava_zip(file: UploadFile = File(...)) -> StreamingResponse:
|
async def upload_strava_zip(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
private: str = Form(default="false"),
|
||||||
|
) -> StreamingResponse:
|
||||||
"""Accept a Strava bulk export ZIP and stream SSE progress while processing.
|
"""Accept a Strava bulk export ZIP and stream SSE progress while processing.
|
||||||
|
|
||||||
The ZIP is written to a temp file, processed activity-by-activity, then deleted.
|
The ZIP is written to a temp file, processed activity-by-activity, then deleted.
|
||||||
@@ -795,6 +798,8 @@ async def upload_strava_zip(file: UploadFile = File(...)) -> StreamingResponse:
|
|||||||
if not file.filename or not file.filename.lower().endswith(".zip"):
|
if not file.filename or not file.filename.lower().endswith(".zip"):
|
||||||
raise HTTPException(400, "Please upload a .zip file")
|
raise HTTPException(400, "Please upload a .zip file")
|
||||||
|
|
||||||
|
privacy = "private" if private.lower() in ("true", "1", "yes") else "public"
|
||||||
|
|
||||||
dd = _get_data_dir()
|
dd = _get_data_dir()
|
||||||
import tempfile
|
import tempfile
|
||||||
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False, dir=dd)
|
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False, dir=dd)
|
||||||
@@ -811,7 +816,7 @@ async def upload_strava_zip(file: UploadFile = File(...)) -> StreamingResponse:
|
|||||||
def event_stream():
|
def event_stream():
|
||||||
any_imported = False
|
any_imported = False
|
||||||
try:
|
try:
|
||||||
for event in strava_zip_iter(zip_path, dd):
|
for event in strava_zip_iter(zip_path, dd, privacy=privacy):
|
||||||
yield f"data: {json.dumps(event)}\n\n"
|
yield f"data: {json.dumps(event)}\n\n"
|
||||||
if event.get("type") == "progress" and event.get("status") == "imported":
|
if event.get("type") == "progress" and event.get("status") == "imported":
|
||||||
any_imported = True
|
any_imported = True
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ def strava_zip_iter(
|
|||||||
zip_path: Path,
|
zip_path: Path,
|
||||||
data_dir: Path,
|
data_dir: Path,
|
||||||
originals_dir: Optional[Path] = None,
|
originals_dir: Optional[Path] = None,
|
||||||
|
privacy: str = "public",
|
||||||
) -> Generator[dict, None, None]:
|
) -> Generator[dict, None, None]:
|
||||||
"""Process a Strava export ZIP, yielding SSE-style progress dicts.
|
"""Process a Strava export ZIP, yielding SSE-style progress dicts.
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ def strava_zip_iter(
|
|||||||
orig_dest = originals_dir / entry_name
|
orig_dest = originals_dir / entry_name
|
||||||
shutil.copy2(tmp_path, orig_dest)
|
shutil.copy2(tmp_path, orig_dest)
|
||||||
|
|
||||||
ingest_parsed(parsed, data_dir, privacy="public")
|
ingest_parsed(parsed, data_dir, privacy=privacy)
|
||||||
imported += 1
|
imported += 1
|
||||||
yield {"type": "progress", "n": n, "total": total, "name": display_name, "status": "imported"}
|
yield {"type": "progress", "n": n, "total": total, "name": display_name, "status": "imported"}
|
||||||
|
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ async def upload_activity(
|
|||||||
@app.post("/api/upload/strava-zip")
|
@app.post("/api/upload/strava-zip")
|
||||||
async def upload_strava_zip(
|
async def upload_strava_zip(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
private: str = Form(default="false"),
|
||||||
bincio_session: Optional[str] = Cookie(default=None),
|
bincio_session: Optional[str] = Cookie(default=None),
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
"""Accept a Strava bulk export ZIP and stream SSE progress while processing.
|
"""Accept a Strava bulk export ZIP and stream SSE progress while processing.
|
||||||
@@ -601,6 +602,8 @@ async def upload_strava_zip(
|
|||||||
if not file.filename or not file.filename.lower().endswith(".zip"):
|
if not file.filename or not file.filename.lower().endswith(".zip"):
|
||||||
raise HTTPException(400, "Please upload a .zip file")
|
raise HTTPException(400, "Please upload a .zip file")
|
||||||
|
|
||||||
|
privacy = "private" if private.lower() in ("true", "1", "yes") else "public"
|
||||||
|
|
||||||
dd = _get_data_dir() / user.handle
|
dd = _get_data_dir() / user.handle
|
||||||
import tempfile
|
import tempfile
|
||||||
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False, dir=dd)
|
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False, dir=dd)
|
||||||
@@ -617,7 +620,7 @@ async def upload_strava_zip(
|
|||||||
def event_stream():
|
def event_stream():
|
||||||
any_imported = False
|
any_imported = False
|
||||||
try:
|
try:
|
||||||
for event in strava_zip_iter(zip_path, dd):
|
for event in strava_zip_iter(zip_path, dd, privacy=privacy):
|
||||||
yield f"data: {json.dumps(event)}\n\n"
|
yield f"data: {json.dumps(event)}\n\n"
|
||||||
if event.get("type") == "progress" and event.get("status") == "imported":
|
if event.get("type") == "progress" and event.get("status") == "imported":
|
||||||
any_imported = True
|
any_imported = True
|
||||||
|
|||||||
@@ -338,6 +338,11 @@ try {
|
|||||||
<div id="zip-label">Drop your Strava export .zip<br/>or click to browse</div>
|
<div id="zip-label">Drop your Strava export .zip<br/>or click to browse</div>
|
||||||
<input id="zip-input" type="file" accept=".zip" class="hidden" />
|
<input id="zip-input" type="file" accept=".zip" class="hidden" />
|
||||||
</div>
|
</div>
|
||||||
|
<label class="flex items-center gap-2 mt-3 text-xs text-zinc-400 cursor-pointer select-none">
|
||||||
|
<input id="zip-private" type="checkbox" class="accent-blue-500" />
|
||||||
|
Mark all imported activities as private
|
||||||
|
<span class="text-zinc-600">(Strava export doesn't include privacy settings)</span>
|
||||||
|
</label>
|
||||||
<p id="zip-status" class="mt-3 text-xs text-center leading-relaxed" style="min-height: 1.25rem"></p>
|
<p id="zip-status" class="mt-3 text-xs text-center leading-relaxed" style="min-height: 1.25rem"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -447,6 +452,7 @@ try {
|
|||||||
const zipInput = document.getElementById('zip-input');
|
const zipInput = document.getElementById('zip-input');
|
||||||
const zipLabel = document.getElementById('zip-label');
|
const zipLabel = document.getElementById('zip-label');
|
||||||
const zipStatus = document.getElementById('zip-status');
|
const zipStatus = document.getElementById('zip-status');
|
||||||
|
const zipPrivate = document.getElementById('zip-private');
|
||||||
const drop = document.getElementById('upload-drop');
|
const drop = document.getElementById('upload-drop');
|
||||||
const input = document.getElementById('upload-input');
|
const input = document.getElementById('upload-input');
|
||||||
const label = document.getElementById('upload-label');
|
const label = document.getElementById('upload-label');
|
||||||
@@ -690,6 +696,7 @@ try {
|
|||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
|
fd.append('private', zipPrivate?.checked ? 'true' : 'false');
|
||||||
|
|
||||||
// POST the file; server responds with SSE stream immediately after receiving body
|
// POST the file; server responds with SSE stream immediately after receiving body
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|||||||
@@ -47,6 +47,17 @@ import Base from '../../layouts/Base.astro';
|
|||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text: string, done: () => void) {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0';
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.focus();
|
||||||
|
ta.select();
|
||||||
|
try { document.execCommand('copy'); done(); } catch (_) {}
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadInvites() {
|
async function loadInvites() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/invites', { credentials: 'include' });
|
const r = await fetch('/api/invites', { credentials: 'include' });
|
||||||
@@ -66,9 +77,16 @@ import Base from '../../layouts/Base.astro';
|
|||||||
// Copy link buttons
|
// Copy link buttons
|
||||||
listEl.querySelectorAll('.copy-btn').forEach(btn => {
|
listEl.querySelectorAll('.copy-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
navigator.clipboard.writeText((btn as HTMLElement).dataset.link ?? '');
|
const text = (btn as HTMLElement).dataset.link ?? '';
|
||||||
|
const done = () => {
|
||||||
btn.textContent = 'Copied!';
|
btn.textContent = 'Copied!';
|
||||||
setTimeout(() => { btn.textContent = 'Copy link'; }, 2000);
|
setTimeout(() => { btn.textContent = 'Copy link'; }, 2000);
|
||||||
|
};
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(done).catch(() => fallbackCopy(text, done));
|
||||||
|
} else {
|
||||||
|
fallbackCopy(text, done);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user