feat: Phase 8 - Write Operations & Concurrency
Implement safe write operations with concurrency control, partial updates, conflict resolution, and file rename/move with automatic link updates. New Tools: - update_frontmatter: Partial frontmatter updates with concurrency control - update_sections: Line-based section edits to reduce race conditions - rename_file: File rename/move with automatic wikilink updates Enhanced Tools: - create_note: Added onConflict strategies (error, overwrite, rename) - delete_note: Added soft delete, dryRun, and concurrency control Key Features: - ETag-based optimistic locking via ifMatch parameter - Version tracking on all write operations - Conflict resolution strategies - Link integrity maintenance during file operations - Safe operations with preview and recovery options Files Created: - src/utils/version-utils.ts Files Modified: - src/tools/note-tools.ts - src/utils/frontmatter-utils.ts - src/tools/index.ts - src/types/mcp-types.ts - ROADMAP.md - CHANGELOG.md Fixes: - Fixed rename_file backlinks API issue (not available in Obsidian API) - Fixed update_frontmatter null-object error when patch is undefined
This commit is contained in:
58
src/utils/version-utils.ts
Normal file
58
src/utils/version-utils.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { TFile } from 'obsidian';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Version control utilities for concurrency management
|
||||
* Implements ETag-based optimistic locking for write operations
|
||||
*/
|
||||
export class VersionUtils {
|
||||
/**
|
||||
* Generate a version ID (ETag) for a file based on its modification time and size
|
||||
* Format: base64(sha256(mtime + size))
|
||||
*/
|
||||
static generateVersionId(file: TFile): string {
|
||||
const data = `${file.stat.mtime}-${file.stat.size}`;
|
||||
const hash = crypto.createHash('sha256').update(data).digest('base64');
|
||||
// Use URL-safe base64 and truncate to reasonable length
|
||||
return hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').substring(0, 22);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the provided version ID matches the current file version
|
||||
* Returns true if versions match (safe to proceed with write)
|
||||
* Returns false if versions don't match (conflict detected)
|
||||
*/
|
||||
static validateVersion(file: TFile, providedVersionId: string): boolean {
|
||||
const currentVersionId = this.generateVersionId(file);
|
||||
return currentVersionId === providedVersionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a version mismatch error message
|
||||
*/
|
||||
static versionMismatchError(path: string, providedVersion: string, currentVersion: string): string {
|
||||
return JSON.stringify({
|
||||
error: 'Version mismatch (412 Precondition Failed)',
|
||||
path,
|
||||
message: 'The file has been modified since you last read it. Please re-read the file and try again.',
|
||||
providedVersion,
|
||||
currentVersion,
|
||||
troubleshooting: [
|
||||
'Re-read the file to get the latest versionId',
|
||||
'Merge your changes with the current content',
|
||||
'Retry the operation with the new versionId'
|
||||
]
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response with version information
|
||||
*/
|
||||
static createVersionedResponse(file: TFile, data: any): any {
|
||||
return {
|
||||
...data,
|
||||
versionId: this.generateVersionId(file),
|
||||
modified: file.stat.mtime
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user