Add Web Push, guided start screen, SVG icons, chunked uploads, rename
- Web Push (VAPID + RFC 8291, pure Rust): permission/question/finished notifications while no app window is visible; settings with test button - Guided new-session flow: task, craft target (website/app/game/old software/fresh), clean room or deep, description, model, folder (new, existing or cloned git repository); originals upload to original/ - Craft phase bar (craft_phase tool) for study/specify/build/check/ready - Chunked uploads (8 MB) up to server.max_upload_mb (default 500 MB) - All UI glyphs and emojis replaced by themeable SVG icons - Rename dialog with manual name or model-generated suggestion Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
013695722a
commit
a0d0e707f2
48 files changed
+2724
-228
No files matched your search
+22
-3
@@ -373,16 +373,35 @@ pub async fn remove(Extension(user): Extension<User>, State(app): State<AppState
|
||||
pub struct UploadQuery {
|
||||
/// Target file path (absolute).
|
||||
path: String,
|
||||
/// Chunked upload: byte offset of this chunk (0 = first chunk, creates the file).
|
||||
offset: Option<u64>,
|
||||
}
|
||||
|
||||
/// Upload one file as the raw request body.
|
||||
/// Upload a file as the raw request body – whole, or in chunks with `offset`
|
||||
/// (large files: each chunk stays below the reverse proxy's body limit).
|
||||
pub async fn upload(Extension(user): Extension<User>, State(app): State<AppState>, Query(q): Query<UploadQuery>, body: Bytes) -> ApiResult {
|
||||
let p = checked_path(&app, &user, &q.path, false)?;
|
||||
if p.is_dir() {
|
||||
return Err(bad("is a directory"));
|
||||
}
|
||||
tokio::fs::write(&p, &body).await?;
|
||||
Ok(Json(json!({"ok": true, "path": p, "size": body.len()})))
|
||||
let max = app.config().server.max_upload_mb.saturating_mul(1024 * 1024);
|
||||
let offset = q.offset.unwrap_or(0);
|
||||
if offset + body.len() as u64 > max {
|
||||
return Err(ApiError(StatusCode::PAYLOAD_TOO_LARGE, format!("file is larger than {} MB", max / 1024 / 1024)));
|
||||
}
|
||||
if offset == 0 {
|
||||
tokio::fs::write(&p, &body).await?;
|
||||
} else {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let current = tokio::fs::metadata(&p).await.map(|m| m.len()).unwrap_or(0);
|
||||
if current != offset {
|
||||
return Err(ApiError(StatusCode::CONFLICT, format!("chunk offset {offset} does not match the uploaded size {current}")));
|
||||
}
|
||||
let mut f = tokio::fs::OpenOptions::new().append(true).open(&p).await?;
|
||||
f.write_all(&body).await?;
|
||||
f.flush().await?;
|
||||
}
|
||||
Ok(Json(json!({"ok": true, "path": p, "size": offset + body.len() as u64})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
Reference in new issue
Block a user