Repository navigation
Commit 978c295
authored
* feat: move workspace to platform cache directory (#12)
The per-document workspace previously lived in a .docx/<stem>/ folder next
to the source file, cluttering user directories and risking collisions
between same-named documents.
It now lives under the platform user cache, in docx-editor/<hash>/ where
<hash> is sha256 of the document's absolute path:
Linux $XDG_CACHE_HOME/docx-editor (fallback ~/.cache/docx-editor)
macOS ~/Library/Caches/docx-editor
Windows %LOCALAPPDATA%\docx-editor (fallback ~/AppData/Local)
The base directory can be overridden, in precedence order, by the
workspace_dir argument to Document.open()/Workspace(), the
DOCX_EDITOR_WORKSPACE_DIR environment variable, or the platform default.
A relative override resolves against the source document's directory, so
workspace_dir=".docx" reproduces the old next-to-file layout for debugging.
Path resolution is centralized in Workspace._resolve_workspace_path(),
replacing the duplicated logic in __init__, exists(), and delete(). Base
directory cleanup suppresses OSError, since the base is now shared across
documents and processes.
No auto-migration: workspaces created by older versions are not found, and
leftover .docx/ folders can be deleted.
* fix: harden workspace cache path resolution and cleanup
Follow-up to the cache migration, from a multi-reviewer pass.
Crash: the workspace dir was keyed by sha256(str(path).encode()), but
str(Path) carries undecodable filename bytes as surrogates, which strict
UTF-8 rejects. Opening e.g. "café.docx" (latin-1 on disk) raised
UnicodeEncodeError. Hash via os.fsencode(os.path.normcase(path)) instead;
this is byte-identical for all decodable paths, so no workspace moves.
normcase also folds case on Windows, where one file could otherwise map to
two workspaces.
Data loss: close()/delete() removed the base directory when it was left
empty. That was safe when the base was the library's own .docx/ folder, but
the base is now the shared cache root -- or whatever directory the caller
passed via workspace_dir=/DOCX_EDITOR_WORKSPACE_DIR. The library was
deleting a directory it did not create. It now removes only the document's
own workspace folder. Two tests asserted the old behavior and are rewritten.
Path resolution: overrides are now tilde-expanded (DOCX_EDITOR_WORKSPACE_DIR
=~/ws created a literal "~" directory next to each document), an empty or
whitespace override counts as unset and falls through to the next precedence
level (workspace_dir="" silently rebased onto the document's own directory),
and a relative XDG_CACHE_HOME/%LOCALAPPDATA% is ignored per the XDG spec
rather than scattering caches beside every document.
Permissions: the workspace holds the document's plaintext in a shared,
predictably-named cache, so it and its base are created 0o700 instead of
inheriting a 0o775 umask default. mkdir(parents=True, mode=) applies the
mode to the leaf only, so the base is created explicitly.
Provenance: a workspace whose meta.json names a different source document is
now refused instead of silently adopted -- adopting it would let a later
save() pack that other document's XML over this one's source file.
Errors: filesystem failures at the new cache boundary (unwritable base, no
resolvable home) surfaced as raw PermissionError/RuntimeError out of
Document.open(). They are now WorkspaceError, naming the override to set.
Also: add a public Document.workspace_path -- the docs told users to inspect
the preserved workspace but gave them no way to locate it now that it is an
opaque hash under the cache. Drop the write-only self.workspace_dir, hoist
the function-local shutil import, and correct the "unsaved edits in the
workspace" wording (edits reach the workspace only on save()).
* fix: correct three regressions in the workspace hardening
Found by a second adversarial pass over the previous commit.
The permissions test guarded itself with skipif(os.getuid() == 0). A skipif
condition is evaluated at collection time on every platform, and os.getuid
does not exist on Windows, so the whole test module would die on import there
-- taking with it the two tests that exist specifically to cover the Windows
cache dir. Guard on os.name first; `or` short-circuits before os.geteuid.
The provenance check compared the raw source path while the workspace key is
normcase-folded. On a case-insensitive filesystem two spellings map to one
workspace directory, which the check would then reject as "a different
document" -- and the message tells the user to pass force_recreate=True,
which would rmtree the workspace. Fold both sides the same way.
Stripping the override with str(override).strip() also stripped Path inputs,
silently redirecting a path that legitimately ends in whitespace to a
different directory. Strip str overrides (where stray whitespace is config
noise) and take a Path verbatim.
Also document the WorkspaceError exits that the hardening added to
Document.open(), and drop the literal ~ from the workspace_path example --
the value is always expanded.
1 parent d83a4e4 commit 978c295
9 files changed
Lines changed: 542 additions & 75 deletions
File tree
- docs
- docx_editor
- openspec
- skills/docx
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
| 21 | + | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
27 | | - | |
| 28 | + | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
36 | 61 | | |
37 | 62 | | |
38 | 63 | | |
| |||
51 | 76 | | |
52 | 77 | | |
53 | 78 | | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
54 | 87 | | |
55 | 88 | | |
56 | 89 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
32 | 46 | | |
33 | 47 | | |
34 | 48 | | |
| |||
203 | 217 | | |
204 | 218 | | |
205 | 219 | | |
206 | | - | |
| 220 | + | |
207 | 221 | | |
208 | 222 | | |
209 | 223 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
89 | | - | |
90 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
91 | 97 | | |
92 | 98 | | |
93 | 99 | | |
94 | 100 | | |
95 | 101 | | |
96 | | - | |
97 | | - | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
98 | 111 | | |
99 | 112 | | |
100 | 113 | | |
101 | 114 | | |
102 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
103 | 119 | | |
104 | | - | |
105 | | - | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
106 | 123 | | |
107 | 124 | | |
108 | 125 | | |
| |||
111 | 128 | | |
112 | 129 | | |
113 | 130 | | |
114 | | - | |
| 131 | + | |
115 | 132 | | |
116 | | - | |
| 133 | + | |
117 | 134 | | |
118 | 135 | | |
119 | 136 | | |
| |||
126 | 143 | | |
127 | 144 | | |
128 | 145 | | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
129 | 156 | | |
130 | 157 | | |
131 | 158 | | |
| |||
0 commit comments