feat: v0.8.0 edit and save files with conflict detection
- devdesk attach --fs-write SESSION PATH BYTES BASESHA|new: streamed like uploads, temp file in the same folder, sha re-check (conflict reply instead of overwriting), mode/owner kept, atomic mv; new files with folders, never outside the session folder or through a symlink out of it. - Editor: editable CodeMirror for UTF-8 text, dirty tracking, Ctrl+S, Save/Revert, conflict dialog with a disk-vs-yours diff, changed-on-disk banner, drafts in localStorage (flushed on pagehide), close/leave guards, New file. - Upload streaming factored into streamIn(), shared by uploads and saves. Closes #20 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -620,3 +620,58 @@ blob_body() {
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$(jq -r '[(.entries | length), .truncated] | @tsv' <<<"$output")" = "$(printf '5\ttrue')" ]
|
||||
}
|
||||
|
||||
# Save through --fs-write the way the browser does: content as base64 on stdin.
|
||||
fs_write() { # SESSION PATH BASE CONTENT-FILE
|
||||
local size
|
||||
size=$(stat -c %s "$4")
|
||||
base64 -w0 "$4" | devdesk attach --fs-write "$1" "$2" "$size" "$3"
|
||||
}
|
||||
|
||||
@test "fs-write saves atomically, keeps the mode, and refuses stale bases (#20)" {
|
||||
p="$DEVDESK_PROJECTS_ROOT/alpha"
|
||||
printf 'v1\n' >"$p/conf.sh"
|
||||
chmod 0750 "$p/conf.sh"
|
||||
base=$(sha256sum "$p/conf.sh" | cut -d' ' -f1)
|
||||
printf 'v2\n' >"$TMP/new"
|
||||
run fs_write alpha conf.sh "$base" "$TMP/new"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$(tail -n1 <<<"$output" | jq -r '.ok')" = true ]
|
||||
[ "$(cat "$p/conf.sh")" = v2 ]
|
||||
[ "$(stat -c %a "$p/conf.sh")" = 750 ]
|
||||
[ "$(tail -n1 <<<"$output" | jq -r '.sha')" = "$(sha256sum "$p/conf.sh" | cut -d' ' -f1)" ]
|
||||
# Someone (Claude) changed it since: the old base no longer matches.
|
||||
printf 'v3 from me\n' >"$TMP/mine"
|
||||
run fs_write alpha conf.sh "$base" "$TMP/mine"
|
||||
[ "$(tail -n1 <<<"$output" | jq -r '[.ok, .conflict] | @tsv')" = "$(printf 'false\ttrue')" ]
|
||||
[ "$(cat "$p/conf.sh")" = v2 ]
|
||||
# No temp files left behind
|
||||
[ -z "$(find "$p" -name '*.devdesk-*')" ]
|
||||
# Empty files are fine
|
||||
: >"$TMP/empty"
|
||||
base=$(sha256sum "$p/conf.sh" | cut -d' ' -f1)
|
||||
run fs_write alpha conf.sh "$base" "$TMP/empty"
|
||||
[ "$(tail -n1 <<<"$output" | jq -r '.ok')" = true ]
|
||||
[ ! -s "$p/conf.sh" ]
|
||||
}
|
||||
|
||||
@test "fs-write creates new files (and folders) but never overwrites or escapes (#20)" {
|
||||
p="$DEVDESK_PROJECTS_ROOT/alpha"
|
||||
printf 'hello\n' >"$TMP/c"
|
||||
run fs_write alpha docs/notes/new.md new "$TMP/c"
|
||||
[ "$(tail -n1 <<<"$output" | jq -r '[.ok, .path] | @tsv')" = "$(printf 'true\tdocs/notes/new.md')" ]
|
||||
[ "$(cat "$p/docs/notes/new.md")" = hello ]
|
||||
run fs_write alpha docs/notes/new.md new "$TMP/c"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ $output == *"already exists"* ]]
|
||||
ln -s "$TMP/outside" "$p/out"
|
||||
for bad in ../escape.txt /etc/x out/pwned.txt out/newdir/x.txt docs/ ""; do
|
||||
run fs_write alpha "$bad" new "$TMP/c"
|
||||
[ "$status" -ne 0 ]
|
||||
done
|
||||
[ ! -e "$TMP/outside/pwned.txt" ] && [ ! -e "$TMP/outside/newdir" ] && [ ! -e "$DEVDESK_PROJECTS_ROOT/escape.txt" ]
|
||||
# A short stream fails without touching anything
|
||||
run bash -c "printf aGVs | '$REPO/bin/devdesk' attach --fs-write alpha docs/notes/new.md 6 $(sha256sum "$p/docs/notes/new.md" | cut -d' ' -f1)"
|
||||
[ "$status" -ne 0 ]
|
||||
[ "$(cat "$p/docs/notes/new.md")" = hello ]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user