Commit 3dba89d
Add "move" operation via
* feat: implement file move operation
- Add move operation to PATCH endpoint for files
- Use Operation: move, Target-Type: file, Target: path
- Automatically creates parent directories if needed
- Preserves all internal links using FileManager.renameFile()
- Validates paths and prevents overwrites
- Add tests for edge cases
* refactor: simplify PATCH endpoint validation logic
- Add new error codes for clearer validation messages
- Use consistent returnCannedResponse pattern throughout
- Reorder validation checks for better flow
- Separate file operations from applyPatch operations early
- Maintain upstream coding style and patterns
* feat: add path validation security and consistent error handling to file move
- Add path traversal protection to prevent access outside vault
- Use returnCannedResponse consistently throughout handleMoveOperation
- Add proper error codes to ErrorCode enum and ERROR_CODE_MESSAGES
- Validate paths early to prevent directory path destinations
- Check parent directory existence before creating
- Add comprehensive security tests for path traversal attempts
- Normalize paths to handle backslashes and multiple slashes
* docs: Add OpenAPI documentation for file move operation
- Add move operation to PATCH endpoint enum
- Update Target parameter description for move operations
- Include example for file move functionality
- Documents Operation: move, Target-Type: file usage
* Implement a custom MOVE HTTP method
* Place MOVE under additionalOperations in OpenAPI spec
Stoplight Elements (forked build) expects non-standard HTTP methods
to be nested under an `additionalOperations` key on the path item
rather than as direct siblings of `get`/`post`/etc. Without this,
the MOVE operation is ignored by the doc renderer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Refine MOVE endpoint headers and destination handling
- Rename Overwrite: T/F to Allow-Overwrite: true/false to match the
boolean header convention used elsewhere in this API (e.g.
Create-Target-If-Missing, Reject-If-Content-Preexists). Defaults to
false, which preserves the safe no-overwrite behaviour.
- Treat a trailing slash in the Destination header as a directory
target: the source filename is appended automatically, so
"Destination: archive/" moves "notes/todo.md" to "archive/todo.md".
This mirrors the behaviour of Unix mv and removes an otherwise
confusing 400 error.
- Move path normalisation (backslash conversion, slash collapsing)
before the traversal check so that a Windows-style path cannot
bypass the leading-slash guard.
- Fix a bug in the .all() route registration where this.handle(...)
returned a middleware function that was never called; requests were
silently dropped, causing all MOVE tests to time out.
- Remove the now-unreachable InvalidDestinationPath error code and fix
the numeric ordering of the remaining new error codes in types.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Refactor MOVE into VaultOperations and add vault_move MCP tool
Moves the core file-rename logic out of RequestHandler._vaultMove and into a
new VaultOperations.moveVaultFile method, following the same pattern used for
all other vault operations (read, write, append, delete, patch). The HTTP
handler now owns only the HTTP-specific concerns: header parsing, path
normalisation, and mapping typed errors to status codes.
Adds a vault_move MCP tool that exposes the same operation to MCP clients,
including path-traversal validation and trailing-slash destination handling
(preserves source filename when destination ends with '/').
Introduces DestinationAlreadyExistsError alongside the existing
FileNotFoundError so callers can distinguish the two failure modes without
inspecting error messages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Change MOVE endpoint response from 201 to 204
The 201 + JSON body (message, oldPath, newPath) was borrowed from WebDAV
semantics, but those fields carry no new information — the caller already
knows both paths. 204 No Content matches the pattern used by every other
mutating endpoint in this API (PUT, DELETE, PATCH on non-patch operations).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Content-Location header to MOVE 204 response
Sets Content-Location to the vault-relative path of the file at its new
location (e.g. "archive/file.md"), matching the convention already used
by the active and periodic routes.
Also corrects two test assertions whose expected error codes didn't match
the current ErrorCode enum.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Document and test Content-Location on active file routes
The Content-Location header was already being set on all /active/ responses
(via redirectToVaultPath and direct res.set calls), but had no unit test
coverage, no OpenAPI documentation, and a loose integration test assertion.
Adds unit tests for all five verbs (GET, PUT, POST, PATCH, DELETE), adds
Content-Location header documentation to the OpenAPI spec for each
corresponding success response, and tightens the integration test assertion
to check the exact vault-relative path rather than just truthiness.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Document and test Content-Location on periodic note routes
Mirrors the active-file commit: Content-Location was already being set on
all /periodic/ responses but had no unit test coverage, no OpenAPI
documentation, and a loose integration test assertion.
Adds unit tests for all five verbs on the current-period route, adds
Content-Location header documentation to the OpenAPI spec for both the
current-period and date-specific variants of each operation, and tightens
the integration test to verify the header value matches a Markdown file path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add integration tests for MOVE endpoint and vault_move MCP tool
Covers the happy path (file moves, source gone, dest has original
content), trailing-slash destination resolution, missing/invalid
input errors (404, 400, 409), and Allow-Overwrite / allowOverwrite
overwrite semantics for both the REST and MCP surfaces.
Running the tests revealed that Allow-Overwrite: true was silently
producing a 500 because renameFile cannot overwrite an existing file.
Fixed moveVaultFile to delete the destination via adapter.remove
before renaming when allowOverwrite is true, consistent with the
pattern already used in deleteVaultFile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Document vault_move in MCP endpoint description
The vault_move tool was missing from the Available Tools table in the
/mcp/ endpoint documentation. Added the entry and regenerated
docs/openapi.yaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Document vault_move in README MCP tools table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Return 400 on malformed percent-encoding in Destination header
decodeURIComponent throws a URIError on invalid sequences like '%E0%'.
Without a try/catch this bubbled into the generic error handler and
produced a 500. Wrap the decode step and return a new
InvalidDestinationHeader (40022) error code instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Treat empty Destination header as vault-root target in MOVE
A Destination header consisting entirely of whitespace was previously
rejected with MissingDestinationHeader (400) because Node.js HTTP
strips OWS from field values before Express sees them, leaving an
empty string that the original !rawDestination guard caught.
Change the guard to rawDestination === undefined so that an explicitly
sent (but empty) Destination header is allowed through. After
normalization the empty string maps to the vault root, and the existing
!normalized branch in the newPath calculation appends the source
filename, moving the file to the root of the vault with its name
preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Return immediately from the MOVE branch in the vault .all() handler
Without the return, control falls through after handle() completes,
making the control flow harder to reason about and creating a risk
that a future edit adds code after the if/else that would execute
even for handled MOVE requests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Guard against data loss when source and destination paths are equal in moveVaultFile
When allowOverwrite was true and destination equaled source, the
overwrite branch deleted the destination (which is the same file as
the source) before calling renameFile on the now-deleted handle,
silently destroying the file.
Add an early return when sourcePath === destinationPath, treating
a same-path move as a no-op. An integration test covers the
allowOverwrite variant that previously caused the deletion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Declare OpenAPI document version as 3.2.0
The additionalOperations field used to document the MOVE verb is a
standard OpenAPI 3.2 Path Item field. The document was previously
declared as 3.0.2, making that field a spec violation. No schema
content changes are needed: none of the 3.0→3.1 breaking constructs
(nullable, boolean exclusiveMaximum/Minimum, format:binary/base64)
appear in this document.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add missing ERROR_CODE_MESSAGES entry for InvalidDestinationHeader
ErrorCode.InvalidDestinationHeader (40022) was defined in types.ts but
had no corresponding entry in the ERROR_CODE_MESSAGES Record, causing
TypeScript to emit a compile error and MOVE error responses for malformed
Destination headers to include an undefined message string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Trim destination in vault_move before normalization
The HTTP MOVE handler trims the raw Destination header value before
normalization; the MCP vault_move tool was missing the equivalent step.
A whitespace-only destination would pass the traversal check and reach
moveVaultFile as a blank path, causing a runtime failure. Adding trim()
aligns MCP behavior with the HTTP handler.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Replace substring-based traversal check with resolve-and-contain
The previous check (normalized.includes("..") || normalized.startsWith("/"))
was too broad: it rejected legitimate filenames containing ".." as a
substring (e.g. "notes..md") and was redundant for the leading-slash case
once normalization was in place.
The new check uses posix.resolve against a synthetic vault root and
asserts the resolved path starts with that root. This correctly handles
all traversal attempts (including multi-segment ".." sequences and
absolute paths) while allowing filenames that merely happen to contain
".." as a substring.
Applied consistently in both the HTTP MOVE handler and the MCP vault_move
tool. Tests added for both the rejection and the newly-allowed case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Reject Destination paths starting with '/' before traversal check
posix.resolve treats its second argument as absolute when it starts with
'/', ignoring the synthetic root entirely. A destination like
/vault/notes/file.md resolves to itself, passes the startsWith check,
and reaches Obsidian's API with an absolute-looking path it does not
understand. Adding an explicit leading-slash guard before the
posix.resolve step ensures any absolute path — including those that
happen to share the synthetic-root prefix — is rejected with
PathTraversalNotAllowed (HTTP) or the equivalent MCP error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Document percent-encoding requirement for Destination header
The Destination header is decoded server-side with decodeURIComponent,
matching how the Target header works on PATCH endpoints. HTTP/1.1
headers are Latin-1 by default, so raw UTF-8 bytes in header values
will be misinterpreted; clients must percent-encode non-ASCII characters
(e.g. r%C3%A9sum%C3%A9.md for résumé.md). Adds the same guidance
already present in the Target header docs to the Destination header
description, and notes that Content-Location values for non-ASCII paths
will likewise be percent-encoded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix missing trailing slash in periodic note integration test URLs
The /periodic/:period/ routes require a trailing slash. All test calls
used /periodic/daily without one, producing a 404 from Express before
the handler was ever reached. This was a pre-existing bug on main.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Return actual post-rename path from moveVaultFile; document Content-Location encoding
moveVaultFile now returns the path Obsidian reports after renameFile
completes (sourceFile.path), matching the pattern every other endpoint
uses — reading the canonical path from the TFile object rather than
echoing back the client-provided string. Both the REST Content-Location
header and the MCP newPath response field now reflect this value.
Also adds a percent-encoding note to the shared ContentLocationHeader
description in the OpenAPI spec, so all endpoints consistently document
that non-ASCII names appear percent-encoded in that header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Guard moveVaultFile against empty destination path
Adds an early check that rejects empty-string destinations before any
vault adapter calls are made. This prevents adapter operations on an
invalid path when a caller bypasses the normalization logic that would
normally resolve an empty destination to a vault-root move.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Align MCP vault_move empty-destination handling with REST behavior
When the caller passes an empty or whitespace-only destination, the REST
MOVE handler resolves it to the vault root by appending the source
filename (matching the trailing-slash convention). The MCP handler was
missing the !normalized guard, so it forwarded an empty string to
moveVaultFile instead. This commit adds the same guard and covers the
case with two new tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Correct MOVE destination docs: escape check, not dot-dot check
The documentation in move.jsonnet and the vault_move MCP tool
description both said the destination "must not contain '..'" — but the
actual validation resolves the path against a synthetic vault root and
rejects only paths that escape it. That means a/../b.md is accepted
(resolves within the vault) while ../../etc/passwd is rejected.
Update both the Destination header description and the 400 response
description in move.jsonnet to reflect the real rule, and regenerate
docs/openapi.yaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Use realistic mock return value in vault_move dot-dot substring test
The test was mocking moveVaultFile to resolve undefined, which is not a
valid return type and masked any assertion on the returned newPath.
Mock it with the real destination string and assert on newPath so the
test exercises the full round-trip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Guillaume Duquesnay <guillaume.duquesnay@gmail.com>
Co-authored-by: Adam Coddington <me@adamcoddington.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>MOVE HTTP verb (#191)1 parent 17a1ca8 commit 3dba89d
16 files changed
Lines changed: 1186 additions & 40 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
| 267 | + | |
267 | 268 | | |
268 | 269 | | |
269 | 270 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
82 | | - | |
| 82 | + | |
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
126 | 132 | | |
127 | 133 | | |
128 | 134 | | |
| |||
256 | 262 | | |
257 | 263 | | |
258 | 264 | | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
259 | 271 | | |
260 | 272 | | |
261 | 273 | | |
| |||
546 | 558 | | |
547 | 559 | | |
548 | 560 | | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
549 | 567 | | |
550 | 568 | | |
551 | 569 | | |
| |||
683 | 701 | | |
684 | 702 | | |
685 | 703 | | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
686 | 710 | | |
687 | 711 | | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
688 | 718 | | |
689 | 719 | | |
690 | 720 | | |
| |||
805 | 835 | | |
806 | 836 | | |
807 | 837 | | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
808 | 844 | | |
809 | 845 | | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
810 | 852 | | |
811 | 853 | | |
812 | 854 | | |
| |||
940 | 982 | | |
941 | 983 | | |
942 | 984 | | |
| 985 | + | |
943 | 986 | | |
944 | 987 | | |
945 | 988 | | |
| |||
1139 | 1182 | | |
1140 | 1183 | | |
1141 | 1184 | | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
1142 | 1191 | | |
1143 | 1192 | | |
1144 | 1193 | | |
| |||
1285 | 1334 | | |
1286 | 1335 | | |
1287 | 1336 | | |
| 1337 | + | |
| 1338 | + | |
| 1339 | + | |
| 1340 | + | |
| 1341 | + | |
| 1342 | + | |
1288 | 1343 | | |
1289 | 1344 | | |
1290 | 1345 | | |
| |||
1588 | 1643 | | |
1589 | 1644 | | |
1590 | 1645 | | |
| 1646 | + | |
| 1647 | + | |
| 1648 | + | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
1591 | 1652 | | |
1592 | 1653 | | |
1593 | 1654 | | |
| |||
1738 | 1799 | | |
1739 | 1800 | | |
1740 | 1801 | | |
| 1802 | + | |
| 1803 | + | |
| 1804 | + | |
| 1805 | + | |
| 1806 | + | |
| 1807 | + | |
1741 | 1808 | | |
1742 | 1809 | | |
| 1810 | + | |
| 1811 | + | |
| 1812 | + | |
| 1813 | + | |
| 1814 | + | |
| 1815 | + | |
1743 | 1816 | | |
1744 | 1817 | | |
1745 | 1818 | | |
| |||
1873 | 1946 | | |
1874 | 1947 | | |
1875 | 1948 | | |
| 1949 | + | |
| 1950 | + | |
| 1951 | + | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
1876 | 1955 | | |
1877 | 1956 | | |
| 1957 | + | |
| 1958 | + | |
| 1959 | + | |
| 1960 | + | |
| 1961 | + | |
| 1962 | + | |
1878 | 1963 | | |
1879 | 1964 | | |
1880 | 1965 | | |
| |||
1932 | 2017 | | |
1933 | 2018 | | |
1934 | 2019 | | |
| 2020 | + | |
| 2021 | + | |
| 2022 | + | |
| 2023 | + | |
| 2024 | + | |
| 2025 | + | |
1935 | 2026 | | |
1936 | 2027 | | |
1937 | 2028 | | |
| |||
2096 | 2187 | | |
2097 | 2188 | | |
2098 | 2189 | | |
| 2190 | + | |
| 2191 | + | |
| 2192 | + | |
| 2193 | + | |
| 2194 | + | |
| 2195 | + | |
2099 | 2196 | | |
2100 | 2197 | | |
2101 | 2198 | | |
| |||
2417 | 2514 | | |
2418 | 2515 | | |
2419 | 2516 | | |
| 2517 | + | |
| 2518 | + | |
| 2519 | + | |
| 2520 | + | |
| 2521 | + | |
| 2522 | + | |
2420 | 2523 | | |
2421 | 2524 | | |
2422 | 2525 | | |
| |||
2585 | 2688 | | |
2586 | 2689 | | |
2587 | 2690 | | |
| 2691 | + | |
| 2692 | + | |
| 2693 | + | |
| 2694 | + | |
| 2695 | + | |
| 2696 | + | |
2588 | 2697 | | |
2589 | 2698 | | |
| 2699 | + | |
| 2700 | + | |
| 2701 | + | |
| 2702 | + | |
| 2703 | + | |
| 2704 | + | |
2590 | 2705 | | |
2591 | 2706 | | |
2592 | 2707 | | |
| |||
2738 | 2853 | | |
2739 | 2854 | | |
2740 | 2855 | | |
| 2856 | + | |
| 2857 | + | |
| 2858 | + | |
| 2859 | + | |
| 2860 | + | |
| 2861 | + | |
2741 | 2862 | | |
2742 | 2863 | | |
| 2864 | + | |
| 2865 | + | |
| 2866 | + | |
| 2867 | + | |
| 2868 | + | |
| 2869 | + | |
2743 | 2870 | | |
2744 | 2871 | | |
2745 | 2872 | | |
| |||
2986 | 3113 | | |
2987 | 3114 | | |
2988 | 3115 | | |
| 3116 | + | |
| 3117 | + | |
| 3118 | + | |
| 3119 | + | |
| 3120 | + | |
| 3121 | + | |
| 3122 | + | |
| 3123 | + | |
| 3124 | + | |
| 3125 | + | |
| 3126 | + | |
| 3127 | + | |
| 3128 | + | |
| 3129 | + | |
| 3130 | + | |
| 3131 | + | |
| 3132 | + | |
| 3133 | + | |
| 3134 | + | |
| 3135 | + | |
| 3136 | + | |
| 3137 | + | |
| 3138 | + | |
| 3139 | + | |
| 3140 | + | |
| 3141 | + | |
| 3142 | + | |
| 3143 | + | |
| 3144 | + | |
| 3145 | + | |
| 3146 | + | |
| 3147 | + | |
| 3148 | + | |
| 3149 | + | |
| 3150 | + | |
| 3151 | + | |
| 3152 | + | |
| 3153 | + | |
| 3154 | + | |
| 3155 | + | |
| 3156 | + | |
| 3157 | + | |
| 3158 | + | |
| 3159 | + | |
| 3160 | + | |
| 3161 | + | |
| 3162 | + | |
| 3163 | + | |
| 3164 | + | |
| 3165 | + | |
| 3166 | + | |
| 3167 | + | |
| 3168 | + | |
| 3169 | + | |
| 3170 | + | |
| 3171 | + | |
| 3172 | + | |
| 3173 | + | |
| 3174 | + | |
| 3175 | + | |
| 3176 | + | |
| 3177 | + | |
| 3178 | + | |
| 3179 | + | |
| 3180 | + | |
| 3181 | + | |
| 3182 | + | |
| 3183 | + | |
| 3184 | + | |
2989 | 3185 | | |
2990 | 3186 | | |
2991 | 3187 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
0 commit comments